As I had the first time with Kiwi
What's new?
In this article I want to talk about the application of BDD technology when developing applications for iOS.
It was interesting to try out one of the methodologies in practice: TDD or BDD. The choice fell on the BDD. Why exactly him? It was very interesting to talk about him at DevCamp in the Kharkov office of Ciklum. Why Kiwi? He was also discussed at this notorious DevCamp'e. Therefore, I wanted to try everything myself in practice. So, who are interested in examples with BDD, is a little more complicated than testing a line break or a calculator, I ask for a cat.
Formulation of the problem
What is the most common business application developer for iOS? In my opinion, this is receiving data from the server and displaying it to the user. For example, an application was written that solved the problem of obtaining questions on the iphone tag from the StackOverflow website and then displaying them in a table. I will not delve into the intricacies of architecture and implementation, but rather dwell in more detail on interesting, in my opinion, things.
How to test a request?
Probably, everyone had situations when working in parallel with the web-team, you could not reach the server. The reasons for this could be different, either the url was changed, or the service broke. In general, it doesn’t matter. But what’s important is that it would be good to check when running unit tests, but is our server responding ?! The “StackOverflowRequest” class is responsible for communicating with the server. We need that when receiving data from the server successfully, the “receivedJSON:” method should be called, and if unsuccessful, “fetchFailedWithError:” should be called. As the tested data, we will select a valid and invalid url.
it(@"should recieve receivedJSON", ^
{
NSString *questionsUrlString = @"http://api.stackoverflow.com/1.1/search?tagged=iphone&pagesize=20";
IFStackOverflowRequest *request = [[IFStackOverflowRequest alloc] initWithDelegate:controller urlString:questionsUrlString];
[[request fetchQestions] start];
[[[controller shouldEventuallyBeforeTimingOutAfter(3)] receive] receivedJSON:any()];
});
it(@"should recieve fetchFailedWithError", ^
{
NSString *fakeUrl = @"asda";
IFStackOverflowRequest *request = [[IFStackOverflowRequest alloc] initWithDelegate:controller urlString:fakeUrl];
[[request fetchQestions] start];
[[[controller shouldEventuallyBeforeTimingOutAfter(1)] receive] fetchFailedWithError:any()];
});
Unfortunately, in this test one of the test conditions is violated: tests must pass quickly. And since we are waiting for a response from the server, this is not good. Therefore, I enclosed these 2 tests in the ifdef block.
How to test the wait for data from the server?
As you know, if there is some kind of loading / processing of data, then we should show this to the user so that there is no feeling that the application has freeze. Most often this occurs when downloading pictures from the server. In my case, I just show the spiner. The algorithm by which I determine if pictures have loaded is implemented using KVO.
it(@"spiner should be visible during avatar loading", ^
{
IFQuestionCell *cell = (IFQuestionCell *)[tableDelegate tableView:[UITableView new] cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
UIImageView *askerAvatar = (UIImageView *)[cell objectForPropertyName:@"askerAvatar"];
[askerAvatar.image shouldBeNil];
UIActivityIndicatorView *spiner = (UIActivityIndicatorView *)[cell objectForPropertyName:@"spiner"];
[spiner shouldNotBeNil];
[[theValue(spiner.hidden) should] equal:theValue(NO)];
[[theValue(spiner.isAnimating) should] equal:theValue(YES)];
});
it(@"spiner should be hidden when avatar is loaded", ^
{
IFQuestionCell *cell = (IFQuestionCell *)[tableDelegate tableView:[UITableView new] cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
UIImageView *askerAvatar = (UIImageView *)[cell objectForPropertyName:@"askerAvatar"];
askerAvatar.image = [UIImage new];
UIActivityIndicatorView *spiner = (UIActivityIndicatorView *)[cell objectForPropertyName:@"spiner"];
[spiner shouldNotBeNil];
[[theValue(spiner.hidden) should] equal:theValue(YES)];
});
How to test the correctness of filling the table with data?
For these purposes, I usually use an object that supports the "UITableViewDataSource, UITableViewDelegate" protocols. Perhaps I will not give an example of testing the correctness of sorting and uniqueness of objects in a table in the article, in view of their banal implementation. This can be seen in the example. I'll show you how to test the loading of new questions. As you know, the server cannot give us any amount of data that we request from it. Instead, we get the data in chunks. In my case, this is 20 questions at a time. That is, I can display 20 questions, and after the user has scrolled to the last question, I will have to make a request to the server to get the next batch of questions. In order for the user to see that a request has been sent to the server for the next batch of questions, instead of a cell with questions, I display a cell with spinners.
it(@"spiner cell should be last cell, if last cell is not visible on table", ^
{
IFQuestion *q1 = [IFQuestion new];
IFQuestion *q2 = [IFQuestion new];
IFQuestion *q3 = [IFQuestion new];
IFQuestion *q4 = [IFQuestion new];
IFQuestion *q5 = [IFQuestion new];
IFQuestion *q6 = [IFQuestion new];
IFQuestion *q7 = [IFQuestion new];
IFQuestion *q8 = [IFQuestion new];
q1.questionID = 1;
q2.questionID = 2;
q3.questionID = 3;
q4.questionID = 4;
q5.questionID = 5;
q6.questionID = 6;
q7.questionID = 7;
q8.questionID = 8;
[tableDelegate addQuestions:@[q1, q2, q3, q4, q5, q6, q7, q8]];
IFSpinerCell *cell = (IFSpinerCell *)[tableDelegate tableView:tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:9 inSection:0]];
NSString *cellClassName = NSStringFromClass ([cell class]);
[[cellClassName should] equal:NSStringFromClass ([IFSpinerCell class])];
for(NSInteger i = 0; i < 9; i++)
{
cell = (IFSpinerCell *)[tableDelegate tableView:tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
cellClassName = NSStringFromClass ([cell class]);
[[cellClassName should] equal:NSStringFromClass ([IFQuestionCell class])];
}
});
PS
A complete example can be found by clicking on this link. Hope this article has been helpful. I will be glad to answer your questions, as well as hear comments and suggestions. Thanks.