Test-Driven Development of an iOS App Tutorial

test-driven ios development with swift and test driven development by example ios and test driven ios development source code
Dr.KiranArora Profile Pic
Dr.KiranArora,Canada,Teacher
Published Date:27-10-2017
Your Website URL(Optional)
Comment
Test-Driven Development of an iOS App Over the next few chapters, you’ll see an iOS app developed from initial specification to functional product. Of course, with this book being about test-driven development, the app will be written in a test-first fashion.This chapter defines the application’s speci- fication and sets out the strategy for developing the functionality.At the end of this part of the book, you’ll have a fully working—though by no means full-featured—app, sup- ported by a suite of unit tests.You’ll also have seen how the tests help to design and ptg7913098 implement the app code.The full project is available from https://github.com/iamleeg/ BrowseOverflow, should you want to build the app yourself or even extend it. Product Goal The app, called BrowseOverflow, gives users access to recent questions about iOS devel- opment on the stackoverflow.com website. Users can easily find recently asked questions on relevant topics and see the answers that visitors to the site have contributed. StackOverflow I’m not associated with the company behind Stack Overflow: Stack Overflow Internet Services, Inc. I just think that stackoverflow.com is a great resource for iOS developers. It also happens to have a simple API, making it easy to create a nontrivial demo app in the limited space available in a book like this, and the content is made available under a Creative Commons license. If you have questions about iOS app programming or any other coding topic, stackoverflow.com ought to be your first port of call. You can use Stack Overflow’s API without registering for an API key. Your app will be limited in the number of requests it can make per unit time, so if you intend to distribute a (test- driven) app based on the code here, you should register and get an API key from http://stackapps.com/. http://itbookshub.com/Chapter 5 Test-Driven Development of an iOS App 60 Use Cases On launching BrowseOverflow, the user sees a list of topics, shown in Figure 5.1. Each topic represents a tag on Stack Overflow. Questions are tagged to indicate what subjects they address. ptg7913098 Figure 5.1 As a stack overflow browser, I want to see a list of topics related to iOS development. Tapping on a topic in the list loads a list of the 20 most recently asked questions tagged with that topic, presented in chronological order.A sample list is shown in Figure 5.2. In addition to the titles of the questions, users can see who asked each question (including an avatar image) and the question’s score (how many times it has been voted up and down on the website). http://itbookshub.com/Use Cases 61 ptg7913098 Figure 5.2 As a stack overflow browser, given that I have tapped on a topic, I want to see a list of questions that have been asked on that topic. Retrieving the list of questions clearly requires a network connection, and even if the app has a Wi-Fi or 3G data network available, it’s still possible for the connection to stackoverflow.com to fail. If it does, BrowseOverflow should show a message explaining that current questions are not available (demonstrated in Figure 5.3). http://itbookshub.com/Chapter 5 Test-Driven Development of an iOS App 62 ptg7913098 Figure 5.3 As a stack overflow browser, given that the app encountered a problem, I want to be told about it. Tapping on a question title in one of the lists of questions presents a view showing more information about this question.The full question text is available, along with each of the answers.The accepted answer (if there is one) is indicated with a tick and appears directly below the question. Following this, the other answers are presented in descend- ing order of score.The name and avatar of the writer of each answer is presented along with that answer, as shown in Figure 5.4.As with the question list, BrowseOverflow should display a message explaining that answers aren’t available if it cannot retrieve the information from the Stack Overflow website. www.it-ebooks.info http://itbookshub.com/Plan of Attack 63 ptg7913098 Figure 5.4 As a stack overflow browser, given that I have tapped on a question, I want to see answers people have provided to that question. Because the preceding views represent a master-detail presentation of the information on the website, the overall flow through the app should use standard iOS navigation controls, starting from the tag list, navigating through the question list, to an individual question and its answers. Plan of Attack There are numerous approaches to implement an app like this one. Many developers would develop a single feature at a time, ensuring that all the functionality behind one button works completely before moving on to the next button. Others would work on each view separately, getting all of the functionality in the topics view working before working on the questions list view. In each case, it’s common to take an “outside-in” approach, in which you put the view together first, then enough controller code to allow users to interact with the view as it is envisaged to behave in the app, then finally to flesh out the functionality until the requirement is fully satisfied. www.it-ebooks.info http://itbookshub.com/Chapter 5 Test-Driven Development of an iOS App 64 Whichever way you slice the cake, it’s usual in agile development teams to work on a single story or small collection of stories for a short period of time (usually known as an iteration or a sprint), with the aim of providing satisfactory implementations of those stories at the end of the sprint. I’m going to take a different approach: My goal here is to show how you can work with test-driven development on code that uses the various parts of the iOS APIs.To support that goal, I’m going to develop this app thematically, starting with the model in Chapters 6 and 7, then the controllers in Chapters 8 and 9 (BrowseOverflow does not contain any custom views). Finally, in Chapter 10, I’ll put the classes together to form the complete app. In this example, the preceding limited and well-defined specification should help to 1 avoid any YAGNI that would normally arise from building an app “from the inside out.” If I do find that things go a little weird along the way, I can always refactor That’s one of the benefits of supporting the code with tests; you can make any changes you want and get fast feedback on whether anything broke (and if so, what). Getting Started It’s time to set up the project to manage the BrowseOverflow application and test case sources. Launch Xcode and create a new project based on the Master-Detail Application iOS app template. In the project options, you’ll want to check the Include Unit Tests box, but you won’t need Core Data, so leave that option unchecked.This app will pro- ptg7913098 vide only iPhone views, so choose iPhone for the device family. Choose somewhere to save the project, and create a local git repository if you want to work with that version control system. If you followed along with that paragraph you’ll already have a working app, although it doesn’t really do much.You can build and run it, and enjoy your lovingly crafted prod- uct: an empty table as shown in Figure 5.5. 1. YAGNI is short for “Ya Ain’t Gonna Need It,” a concept introduced in Chapter 2. www.it-ebooks.info http://itbookshub.com/Getting Started 65 ptg7913098 Figure 5.5 A new project, ready to be turned into BrowseOverflow.app. Apple’s Template Code Now that you’ve created a new project, you’ll see that there’s a lot of code in it already. The Master-Detail app target contains a few classes: the app delegate and the view con- trollers. The BrowseOverflow app could make use of both of these classes. Should you go through and create tests for all that template code? The answer is no—or not yet, anyway. Because you haven’t yet gotten around to imple- menting any of the app’s behavior, no requirements exist yet for what that code should do. It can’t possibly be correct or broken (as long as it compiles); it’s just there. When you come to add features to the app, you’ll find that you want to create some behav- ior in these classes. That’s the time to write the tests that test the behavior you want. If you find that the template code already does what you need, that’s great; there’s nothing to write. If the tests fail, you can edit the code until it does what you need. There’s an old hacker maxim that seems appropriate here: “If it ain’t broke, fix it ’til it is.” One thing you will need to change is Apple’s test fixture, which is named BrowseOverflowTests by default. The template code includes a call to STFail(), which means that the tests automatically fail even though you haven’t written any bugs yet You won’t need that fixture at all, so it’s safe to delete the BrowseOverflowTests.h and BrowseOverflowTests.m files from your project. http://itbookshub.com/ The Data Model The first thing I want to implement is the model layer, the objects that represent the information in the BrowseOverflow app. I’ll take another look at the app description from Chapter 5,“Test-Driven Development of an iOS App,” this time with a require- ments engineering hat on. Specifically, I’ll use a technique called domain analysis to see what classes and objects are required in the app. In domain analysis, you look at the requirements with a view to deciding what objects exist in the problem domain and what their responsibilities are. Nouns in the software requirements represent either objects or properties of some object.Verbs repre- ptg7913098 sent actions (that is, methods), and the object and subject of the verb tell you which object calls the method on which other object. The idea is that you can identify the classes and interactions in the problem domain, and then design your software classes and objects to reflect the domain model. Because the software model is based on the problem domain, it’s unlikely that you’ll write code that doesn’t or can’t satisfy the problem conditions. It also has the benefit that it’s easier for coders to talk to users and other stakeholders about the software because you’re all using the same terminology to talk about the same parts of the problem. An example from a different problem domain is an email application. Email has almost completely replaced postal mail, in which a sender wrote a message and addressed it to the reader so that the postal service could determine how to deliver it to—in the context of a business anyway—the recipient’s inbox. Having read the message, the recipi- ent could throw it away or file it into a labeled folder. It is easy to see how the various objects—message, address, inbox and so on—and verbs—writing, reading, delivering— from the problem domain found their way into the software. Topics Enough talk; let’s look at an example from the BrowseOverflow problem domain. One sentence from Chapter 5 says: On launching BrowseOverflow, the user sees a list of topics… Each topic represents a tag on Stack Overflow. Questions are tagged to indicate what subjects they address. www.it-ebooks.info http://itbookshub.com/Chapter 6 The Data Model 68 I shall assume that the user is not part of the software system (it’s hard to sell an app if the only user is the app itself). However, the list of topics should be part of the system. It seems that a ”topic” is an object in the problem domain. It’s time to write a test to prove that the app provides a Topic class. In fact, because Topic will be a class in the app, I’ll create a new TopicTests fixture in the BrowseOverflowTests target (not the app tar- get), and add the test to that: implementation TopicTests - (void)testThatTopicExists Topic newTopic = Topic alloc init; STAssertNotNil(newTopic, "should be able to create a Topic instance"); end You can’t run that test; it won’t even compile.That’s because there isn’t yet a Topic class at all.Add a new NSObject subclass called Topic to the project.When Xcode asks which targets to add the new file to, check both the app target and the test bundle tar- get. (This is required to work around a problem with unit test bundles:They don’t link symbols from their host apps correctly.) Now the class exists, and you can tell the test fixture about it: ptg7913098 import "TopicTests.h" import "Topic.h" implementation TopicTests - (void)testThatTopicExists Topic newTopic = Topic alloc init; STAssertNotNil(newTopic, "should be able to create a Topic instance"); end Run the test again:That change was sufficient to get this test to pass. The user needs to see the topics, so the Topic class needs some property that can represent it in the UI—a textual name seems appropriate.The list of topics is presented to the user at launch, so the objects can be created with the names they’ll have through- out the lifetime of the app. I would like to be able to do this: - (void)testThatTopicCanBeNamed Topic namedTopic = Topic alloc initWithName: "iPhone"; STAssertEqualObjects(namedTopic.name, "iPhone", "the Topic should have the name I gave it"); www.it-ebooks.info http://itbookshub.com/Topics 69 That won’t compile, because the compiler finds that Topic doesn’t have a name prop- erty.Add it to the class interface: 1 property (readonly) NSString name; and to the implementation: synthesize name; Try running the tests again.That last change lets the tests compile, but the sweet smell of success is still beyond our grasp. In fact, the test runner crashes.Why? 2011-02-17 16:04:33.463 BrowseOverflow3146:207 -Topic initWithName:: unrecognized selector sent to instance 0x4e3f980 Add that initializer method to the Topic class. - (id)initWithName:(NSString )newName if ((self = super init)) name = newName copy; return self; Testing Memory Management ptg7913098 The code examples throughout this book rely on Automatic Reference Counting (ARC), a compiler-supported memory management technique introduced with Xcode 4.2 for iOS developers using the iOS 4 and 5 SDKs. Test-driven development doesn’t require ARC, so it’s possible to use the techniques described here with manual memory management (or garbage collected Objective-C on the Mac OS X SDK) should you need to. However, memory management doesn’t really lend itself to automated testing. The only method Foundation classes provide to inspect memory-management code is the reference count, and this is not trustworthy enough to form the basis of a repeatable test. Sometimes the Foundation or UIKit library might want to retain an object; sometimes an object’s reference count doesn’t change when it’s released; sometimes something happens on a different thread to change the retain count. Because of this, asking an object for its reference count or using a mock to find out when retain or release are called is not going to lead to repeatable results. The rules for memory management in an iOS app are very straightforward, and described by Apple at http://developer.apple.com/library/ios/documentation/cocoa/conceptual/ MemoryMgmt/MemoryMgmt.html. Following those rules, and testing for problems using Instruments, are both good ways to ensure that your objects live for the correct amount of time in your app. 1. Notice that we have no requirements describing whether this property should be atomic or nonatomic, so I have used the default behavior. On the other hand, there doesn’t appear to be a need to change a Topic’s name after it’s been created, so it’s appropriate to create a read-only property: The application doesn’t need the (untested) setter method. www.it-ebooks.info http://itbookshub.com/Chapter 6 The Data Model 70 Now the test succeeds, and BrowseOverflow’s topic objects have a name property that works in the way specified by the tests.The one thing we’ve yet to address from the pre- ceding sentence is that a Topic object should identify a tag from the stackoverflow.com question tags. It happens that a tag is just another string, so it can be handled in the same way as the name property. I’ll add another parameter to the initializer, so the test for the tag looks like this: - (void)testThatTopicHasATag Topic taggedTopic = Topic alloc initWithName: "iPhone" tag: "iphone"; STAssertEqualObjects(taggedTopic.tag, "iphone", "Topics need to have tags"); The procedure to get this test passing is the same as with the name:Add the property and the new initializer. This seems like a good time to stand back and look for opportunities to refactor. Currently, each test uses a different initializer:-init,-initWithName: and -initWithName:tag:. In fact, if each test used the “full” two-argument initializer, all the tests would still work and there would be less code in the Topic class. It doesn’t seem like there’s a need for the -initWithName: initializer at all. Change the tests so that the fixture uses only one initializer for Topic: ptg7913098 implementation TopicTests - (void)testThatTopicExists Topic newTopic = Topic alloc initWithName: "iPhone" tag: "iphone"; STAssertNotNil(newTopic, "should be able to create a Topic instance"); - (void)testThatTopicCanBeNamed Topic namedTopic = Topic alloc initWithName: "iPhone" tag: "iphone"; STAssertEqualObjects(namedTopic.name, "iPhone", "the Topic should have the name I gave it"); - (void)testThatTopicHasATag Topic taggedTopic = Topic alloc initWithName: "iPhone" tag: "iphone"; STAssertEqualObjects(taggedTopic.tag, "iphone", "Topics need to have tags"); end www.it-ebooks.info http://itbookshub.com/Topics 71 Now that there’s no need for the -initWithName: initializer, you can delete it from 2 the Topic class. The recent changes bring an interesting observation: It looks like all the tests work with identical Topic instances.They could all use the same instance, defined as part of the fixture. In other words, we could create a single topic instance in -setUp, use it in every test, and then clean it up in -tearDown. Make that change, and the test fixture interface will look like this: import SenTestingKit/SenTestingKit.h import UIKit/UIKit.h class Topic; interface TopicTests : SenTestCase Topic topic; end and the implementation like this: import "TopicTests.h" import "Topic.h" implementation TopicTests ptg7913098 - (void)setUp topic = Topic alloc initWithName: "iPhone" tag: "iphone"; - (void)tearDown topic = nil; - (void)testThatTopicExists STAssertNotNil(topic, "should be able to create a Topic instance"); - (void)testThatTopicCanBeNamed STAssertEqualObjects(topic.name, "iPhone", "the Topic should have the name I gave it"); - (void)testThatTopicHasATag STAssertEqualObjects(topic.tag, "iphone", "the Topic should have the tag I gave it"); end 2. And indeed you should. Because it’s no longer being tested, you don’t want users of the Topic class to rely on it working properly in the future. www.it-ebooks.info http://itbookshub.com/Chapter 6 The Data Model 72 In the -tearDown method, the test fixture’s Topic is set to nil so that the different tests each get a fresh instance (in fact, that’s guaranteed by OCUnit itself; destroying the variable in -tearDown documents that fact explicitly).There’s one remaining thing that a Topic needs. Examine this sentence from the requirements: Tapping on a topic in the list loads a list of the 20 most recently asked questions tagged with that topic, presented in chronological order. There should be a way to get from a topic to “a list of...questions.” Reading on, it appears that questions have a number of different properties associated with them.This suggests that questions should be represented as a class in the problem domain, and that this should be reflected in the software domain by having the Topic class provide access 3 to a list of Questions.The relationship is shown in Figure 6.1 as a UML diagram. Topic name tag recentQuestions Question 1 0..20 ??? ??? Figure 6.1 UML diagram showing the relationship between Topics and Questions. ptg7913098 The topic object needs to do something to give us “a list” of something. Because we haven’t investigated the requirements about questions yet, we can’t say anything about what objects in that list should do. For now, it’s enough to ensure that Topics can provide a list: - (void)testForAListOfQuestions STAssertTrue(topic recentQuestions isKindOfClass: NSArray class, "Topics should provide a list of recent questions"); Of course, that test fails, so implement a new method on Topic to pass the test (and don’t forget to declare the method in Topic.h, too). - (NSArray )recentQuestions return NSArray array; Now the test passes, but it doesn’t seem very satisfying.The requirements call for a “list of questions,” and all the code does is provide a list. Not a list of anything, just an empty list. Before defining the list of questions in more detail, it will be useful to have a better idea of what a question is. 3. UML, the Unified Modeling Language, is a standardized way to graphically model object-oriented systems. More information is available at www.uml.org/. www.it-ebooks.info http://itbookshub.com/Questions 73 Questions Let’s put the Topic class on one side for the moment, because we've gotten to a point in implementing the app’s requirements that needs us to think about what’s required of a question. In fact, we need to do that in order to complete the implementation of Topic. The first thing to notice is the definition of a Topic’s list that we used in the last sec- tion.That specified that the list must be “in chronological order,” so questions must need a date property that can be used for sorting.Question is a new class, so create a new QuestionTests fixture to test it in the BrowseOverflowTests target, and add the first test: implementation QuestionTests - (void)testQuestionHasADate Question question = Question alloc init; STAssertTrue(question.date isKindOfClass: NSDate class, "Question needs to provide its date"); end However, this test doesn’t compile because there isn’t a Question class yet. Here’s the implementation of that class, to be added to both the app and test case targets. For brevi- ptg7913098 ty’s sake the corresponding header file has been left out. implementation Question - (NSDate )date return NSDate date; end That allows the test to pass, but it looks fishy.The iOS SDK documentation tells us that +NSDate date returns the current date, but what we want to know is the date the question was asked. Evidently the test is wrong.What we really need is some way for the code that creates a Question object to set the date at which it was asked.A setter would 4 do the trick, so I’ll update the test: - (void)testQuestionHasADate Question question = Question alloc init; NSDate testDate = NSDate distantPast; question.date = testDate; STAssertEqualObjects(question.date, testDate, "Question needs to provide its date"); 4. As would an initializer parameter, in the same way that the Topic class was designed. This goes to show that testing doesn’t limit the ways in which you can implement required behavior. www.it-ebooks.info http://itbookshub.com/Chapter 6 The Data Model 74 A simple read/write property on the Question class is sufficient to pass this test, and can replace the previous implementation of -date.You may be thinking that this repre- sents a lot of work just to add a one-line property declaration to a class; can’t we assume that Apple’s implementation of synthesize works properly? The point of the preced- ing test is not to show that the property works, but to demonstrate that the property is needed in the application.Test-driven development is helping to design the classes in the app by making us think about how we should fulfill the app’s requirements in code.As a side effect, we get some safety against breaking things in the future. Should we decide later that these classes should provide a dynamic or hand-crafted implementation of the property, this test will ensure we keep the basic requirement that we can get and set the value. What are the other requirements of question objects? Here’s a relevant quote from the requirements: …in addition to the titles of the questions, users can see who asked each question (including an avatar image) and the question’s score (how many times it has been voted up and down on the website). Okay, titles must be easy to do. I’ll move the question instance that was created in the -testQuestionHasADate test into the fixture, and add a test that ensures it has a title string. Similarly with scores, except that a score will be a NSInteger rather than ptg7913098 a string. import "Question.h" implementation QuestionTests Question question; - (void)setUp question = Question alloc init; question.date = NSDate distantPast; question.title = "Do iPhones also dream of electric sheep?"; question.score = 42; - (void)tearDown question = nil; - (void)testQuestionHasADate NSDate testDate = NSDate distantPast; question.date = testDate; STAssertEqualObjects(question.date, testDate, "Question needs to provide its date"); www.it-ebooks.info http://itbookshub.com/People 75 - (void)testQuestionsKeepScore STAssertEquals(question.score, 42, "Questions need a numeric score"); - (void)testQuestionHasATitle STAssertEqualObjects(question.title, "Do iPhones also dream of electric sheep?", "Question should know its title"); end As with the date test, getting this test to pass is trivial because you just need to add a read-write property with the correct name and type to Question. The next stage is to decide what to do with the asker’s name and image, and this is going to require a little more thought. On the face of it, it would appear that a Person is a different thing than a Question in the problem domain. However, the app needs only a couple of properties related to people: a name and an image.Wouldn’t it be easy just to add those properties to Question, so we could ask for question.askerName and question.askerImage? Yes, it would, but read on a little in the requirements: The name and avatar of the writer of each answer is presented along with that answer… ptg7913098 So the app will also need the name and an image of the Person who provides an answer. I’m jumping ahead a little here, but it’s obvious that if the code for the asker’s name and avatar go into Question, we’ll just need to write the same code again for the class that describes answers later. I could have added the properties to Question and then refactored them out to a Person class later, but spotting this association now lets me shortcut that process.This is an example of applying the System Metaphor idea introduced in Chapter 2,“Techniques for Test-Driven Development”; it seems clear that a Person is a top-level concept in this app so should be treated as such during development. People Unless the artist formerly known as Prince changes career and becomes an iOS develop- er, it seems likely that the name of a Person can be represented as a string. Deciding what to do about a Person’s avatar needs a little more thought.There are many ways to represent an image in an iOS app: a URL that can be used to retrieve the image, the image data, or a CGImageRef or UIImage that represents the image directly.Which should the Person class provide? This is to a large extent a matter of personal preference. My opinion is that the URL is a good fit here. It means that the data model is kept comparatively simple, and the logic over when and how to retrieve image contents is dealt with at the controller level. www.it-ebooks.info http://itbookshub.com/Chapter 6 The Data Model 76 With that decision made, the PersonTests fixture is easy to design: implementation PersonTests - (void)setUp person = Person alloc initWithName: "Graham Lee" avatarLocation: "http://example.com/avatar.png"; - (void)tearDown person = nil; - (void)testThatPersonHasTheRightName STAssertEqualObjects(person.name, "Graham Lee", "expecting a person to provide its name"); - (void)testThatPersonHasAnAvatarURL NSURL url = person.avatarURL; STAssertEqualObjects(url absoluteString, "http://example.com/avatar.png", "The Person’s avatar should be represented by a URL"); ptg7913098 end The Person class itself is two straightforward read-only properties, along with this code to initialize the properties: - (id)initWithName:(NSString )aName avatarLocation:(NSString )location if ((self = super init)) name = aName copy; avatarURL = NSURL alloc initWithString: location; return self; Connecting Questions to Other Classes The reason I originally left the Topic alone to start work on Question was that I needed to see what a Question looked like before I could say what a list of questions in chronological order looked like. I didn’t need to go quite as far as I did with Question before turning back to the interaction with Topic: It’s just easier for you, my beloved readers, if I don’t keep changing the subject all over the place. But speaking of changing the subject, let’s add a new feature to Topic. www.it-ebooks.info http://itbookshub.com/Connecting Questions to Other Classes 77 So, how would I like to use code that returns an ordered list of questions, and how should I test that the questions are in the correct order? Unfortunately, although we’re told that the list must be “in chronological order,” the requirements don’t specify in which direction—that is, whether earlier questions should appear before later questions, or vice versa. I’ll follow convention offered by the iOS Mail app, and a number of third-party apps, in putting newer questions near the top of the list: I should make a note to talk to 5 the customer about that requirement. Okay, first I’ll make sure that I can tell a Topic that it has a Question and see the list of questions for a Topic. In fact, before that, I’ll want to check that a Topic to which no questions have been added actually does contain no questions. Here are both addi- tions to TopicTests: - (void)testForInitiallyEmptyQuestionList STAssertEquals(topic recentQuestions count, (NSUInteger)0, "No questions added yet, count should be zero"); - (void)testAddingAQuestionToTheList Question question = Question alloc init; topic addQuestion: question; STAssertEquals(topic recentQuestions count, (NSUInteger)1, "Add a question, and the count of questions should go up"); ptg7913098 The casts are necessary in these tests because OCUnit compares the type of argu- ments to STAssertEquals() in addition to their values. Notice that the test to add a question to the list modifies the topic’s list of questions.Won’t that break other tests that expect an empty list? No. Remember that each test is executed in its own instance of the fixture class, fresh from having run -setUp.The outcome of one test can’t affect the behavior of another. Recall the implementation of -Topic recentQuestions provided in a previous section: It always returns an empty list.That means that the first of these two tests already passes (the empty list has zero members), but the other test does not pass because there’s no -addQuestion: method yet.Add that method and somewhere for it to put the questions: implementation Topic NSArray questions; // … - (id)initWithName:(NSString )newName tag: (NSString )newTag 5. Because the customer is me, I doubt there’ll be an issue. The point I’m making here is that because test-driven development makes you think about the app requirements in detail, you uncover problems like this before you get too far into coding up the objects that implement the problematic requirements. www.it-ebooks.info http://itbookshub.com/Chapter 6 The Data Model 78 if ((self = super init)) name = newName copy; tag = newTag copy; questions = NSArray alloc init; return self; - (void)addQuestion: (Question )question questions = questions arrayByAddingObject: question; end Now the test doesn’t throw an “unrecognized selector” exception, but it still doesn’t pass.That’s because we still need to get the existing -recentQuestions method to return the list of questions. - (NSArray )recentQuestions return questions; Any list with zero or one objects is always guaranteed to be in chronological order. ptg7913098 What about a list of two objects? Let’s try it. - (void)testQuestionsAreListedChronologically Question q1 = Question alloc init; q1.date = NSDate distantPast; Question q2 = Question alloc init; q2.date = NSDate distantFuture; topic addQuestion: q1; topic addQuestion: q2; NSArray questions = topic recentQuestions; Question listedFirst = questions objectAtIndex: 0; Question listedSecond = questions objectAtIndex: 1; STAssertEqualObjects(listedFirst.date laterDate: listedSecond.date, listedFirst.date, "The later question should appear first in the list"); The test fails.This is unsurprising, because we didn’t do anything yet to control the order of the question list. Let’s change the -recentQuestions method so that it sorts the questions by date. www.it-ebooks.info http://itbookshub.com/Connecting Questions to Other Classes 79 - (NSArray )recentQuestions return questions sortedArrayUsingComparator: (id obj1, id obj2) Question q1 = (Question )obj1; Question q2 = (Question )obj2; return q2.date compare: q1.date; ; There—that works. (You can convince yourself of that by writing the complementary test, where the later question is added before the earlier question. It should pass.) That’s nearly everything, but we need to ensure that only the newest 20 questions are shown: - (void)testLimitOfTwentyQuestions Question q1 = Question alloc init; for (NSInteger i = 0; i 25; i++) topic addQuestion: q1; STAssertTrue(topic recentQuestions count 21, "There should never be more than twenty questions"); That doesn’t work.We can see as many questions as have ever been added.There are two obvious ways to limit the number: Either put some logic into -addQuestion: to remove the twenty-first question if too many get added; or return only the first 20 ques- ptg7913098 tions in -recentQuestions no matter how many there really are.The second way is the easiest to get the test to pass, so let’s try that: - (NSArray )recentQuestions NSArray sortedQuestions = questions sortedArrayUsingComparator: (id obj1, id obj2) Question q1 = (Question )obj1; Question q2 = (Question )obj2; return = q2.date compare: q1.date; ; if (sortedQuestions count 21) return sortedQuestions; else return sortedQuestions subarrayWithRange: NSMakeRange(0, 20); That does work, but it doesn’t seem very nice to me.The number of questions stored for any Topic could grow endlessly, and although the app would only see 20 of them, the rest would stay around, consuming memory.That’s not a memory leak, but it is a stale reference problem. Because this app needs to run in the limited-memory environ- ment of an iOS device, it would be good to address this problem straight away. I’ll www.it-ebooks.info http://itbookshub.com/