Avoiding fragile unit tests with NSDateFormatter - cocoa-touch

What is the best practice for testing NSDateFormatter methods? For example, lets say I have a method:
- (NSString *)formatStringFromDate:(NSDate *)date {
NSDateFormatter *f = [[NSDateFormatter alloc] init];
[f setTimeStyle:NSDateFormatterShortStyle];
[f setDateStyle:NSDateFormatterNoStyle];
return [f stringFromDate:date];
}
There are two ways I can think of testing this method using Kiwi:
1) Create the same formatter in the unit test:
it(#"should format a date", ^{
NSDate *date = [NSDate date];
NSDateFormatter *f = [[NSDateFormatter alloc] init];
[f setTimeStyle:NSDateFormatterShortStyle];
[f setDateStyle:NSDateFormatterNoStyle];
[[[testObject formatStringFromDate:date] should] equal:[f stringFromDate:date]];
});
2) Explicitly write the intended output:
it(#"should format a date", ^{
NSDate *date = [NSDate dateWithTimeIntervalSince1970:1385546122];
NSDateFormatter *f = [[NSDateFormatter alloc] init];
[f setTimeStyle:NSDateFormatterShortStyle];
[f setDateStyle:NSDateFormatterNoStyle];
[[[testObject formatStringFromDate:date] should] equal:#"9:55 am"];
});
Now, to me, #1 seems a bit redundant. I know the test will pass as I'm essentially duplicating the method in my unit test.
Method #2 is a non-starter, as it is incredibly fragile. It completely relies on the test devices current locale being what you'd expect.
So my question is: is there a more appropriate method to test this method, or should I just go ahead with test method #1.

As you say in your comment, what you want to test is that the cell's detailTextLabel's text is set to date with the expected format when a date is present.
This can be tested in several different ways, I expose here the one I would go for.
First of all, creating a date formatter each time we want to format a date is not efficient and it makes it more difficult to test.
So what I suggest is to create a date formatter property in your table view controller:
// .h
#property (strong, nonatomic) NSDateFormatter *dateFormatter;
// .m
- (NSDateFormatter *) dateFormatter
{
if (_dateFormatter == nil)
{
_dateFormatter = [[NSDateFormatter alloc] init];
[_dateFormatter setTimeStyle:NSDateFormatterShortStyle];
[_dateFormatter setDateStyle:NSDateFormatterNoStyle];
}
return _dateFormatter;
}
For the date formatter I would create a simple test that verifies the timeStyle and dateStyle. I am not familiar with Kiwi so I use OCUnit assertions:
TableViewController *sut;// Instantiate the table view controller
STAssertTrue(sut.dateFormatter.timeStyle == NSDateFormatterShortStyle, nil);
STAssertTrue(sut.dateFormatter.dateStyle == setDateStyle:NSDateFormatterNoStyle, nil);
After having this, the idea is to create a test that verifies that the cell returned by tableView:cellForRowAtIndexPath: has its detailTextLabel's text set with the string returned by the formatter. As you said testing the string returned by the date formatter is fragile so we can mock it, stub stringFromDate: returning a constant and verify that the detailTextLabel's text is set to that constant.
So we write the test. I'd try to write it with Kiwi mocks, so sorry if I do something wrong - the idea is the important thing:
TableViewController *sut;// Instantiate the table view controller
id mockDateFormatter = [NSDateFormatter mock];
NSString * const kFormattedDate = #"formattedDate";
NSDate * const date = [NSDate date];
[mockDateFormatter stub:#selector(stringFromDate:) andReturn:kFormattedDate withArguments:date,nil];
sut.dateFormatter = mockDateFormatter;
sut.dates = #[date];// As an example we have an array of dates to show. In the real case we would have an array of the objects you want to show in the table view.
[sut view];// In case we have the registered cells...
UITableViewCell *cell = [sut tableView:sut.tableView
cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
STAssertEqualObjects(cell.detailTextLabel.text, kFormattedDate, nil);
And the method to satisfy that test would be something like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// I assume you have a standard cell registered in your table view
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"CellIdentifier"];
NSDate *date = [self.dates objectAtIndex:indexPath.row];
cell.detailTextLabel.text = [self.dateFormatter stringFromDate:date];
return cell;
}
Hope it helps.

Related

Working with Date and Time separately using NSDatePickerView on Xcode

I have a part of app that set date and time of an appointment. For this, I have two NSDatePickerView. The first one I set to NSDatePickerModeDate, while the other one to NSDatePickerModeTime. But actually they should be referring to a same NSDate object inside a NSMutableDictionary entry. I know about NSDatePickerModeDateTime, but I need the date and time to be picked separatedly.
I know how to set up the NSDatePickerView to show and hide and event control and such, but at the event control UIControlEventValueChanged fire for NSDatePickerView, I'm confused on how to code the change for this, and also how to initialise the pickers (datePicker.date = today, timePicker.date = "9:00 AM")
#interface MyViewController () {
NSMutableDictionary *data;
}
#end
#implementation MyViewController
#synthesize datePicker, timePicker;
- (void) viewDidLoad {
[super viewDidLoad];
data = [[NSMutableDictionary alloc] init];
[data setObject:[NSDate date] forKey:#"date"];
datePicker.datePickerMode = UIDatePickerModeDate;
timePicker.datePickerMode = UIDatePickerModeTime;
[datePicker addTarget:self action:#selector(changeDate:) forControlEvents:UIControlEventValueChanged];
[timePicker addTarget:self action:#selector(changeTime:) forControlEvents:UIControlEventValueChanged];
datePicker.date = data[#"date"]; //????
timePicker.date = data[#"date"]; //????
}
- (IBAction) changeDate:(id)sender {
UIDatePickerView *dp = (UIDatePickerView *)sender;
[data setObject:dp.date forKey:#"date"]; //????
}
- (IBAction) changeTime:(id)sender {
UIDatePickerView *tp = (UIDatePickerView *)sender;
[data setObject:tp.date forKey:#"date"]; //????
}
The part that I don't know how to code it is denoted by //????. I've read about NSDateFormatter, NSCalendar, and some kind of date components on some answers, but that was actually making me more confused as it also throws strings and structs into the mix, what to use to do what and when. Please help.
You can set both date pickers to the same date and time. The unused part is there but it isn't displayed and can't be changed. When the user changes the value of one date picker you have to set the other date picker to the same value.
- (IBAction)changeDate:(id)sender {
NSDatePicker *dp = (NSDatePicker *)sender;
[data setObject:dp.dateValue forKey:#"date"];
self.timePicker.dateValue = dp.dateValue;
}
- (IBAction)changeTime:(id)sender {
NSDatePicker *tp = (NSDatePicker *)sender;
[data setObject:tp.dateValue forKey:#"date"];
self.datePicker.dateValue = tp.dateValue;
}
u can try this
///Convert Full date to only date
- (IBAction) changeDate:(UIDatePickerView *)sender {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"MM/dd/yyyy"];
NSString *dateStr = [dateFormatter stringFromDate:[sender date]];
[data setObject:dateStr forKey:#"date"];
}
///convert Date to only time format
- (IBAction) changeTime:(UIDatePickerView *)sender {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"hh:mm a"];
NSString *dateStr = [dateFormatter stringFromDate:[sender date]];
[data setObject:dateStr forKey:#"Time"];
}

how to use nsdate string objec for later use?

I'm so fedup with NSDate string object.
currently I am generating an unique id on the bases of NSDate as follows:
NSDate *current_date = [[NSDate date]retain];
NSDateFormatter *df = [[NSDateFormatter alloc]init];
[df setDateFormat:#"HHmmssddMMYY"];
NSString *unique_id=[df stringFromDate:current_date];
NSString * current_Test_id=[NSString stringWithString:unique_id];
NSLog(#"current_test_idString %#",current_Test_id);
The code above is generating unique id and prints successfully but if I am printing or accessing currtent_Test_id in another IBAction method then app crashes.
stringWithString will create an autorelease string, modify your code as
NSString * current_Test_id = [[NSString stringWithString:unique_id]retain];
Use this Method
- (NSString *)stringDateFromDate: (NSDate *) date{
NSDateFormatter *df = [[NSDateFormatter alloc]init];
[df setDateFormat:#"HHmmssddMMYY"];
NSString *current_Test_id=[NSString stringWithString:[df stringFromDate:date]];
[df release];
NSLog(#"current_tst_id %#",current_Test_id);
return current_Test_id;
}
Call Method Like that
NSString *current_tst_id = [self stringDateFromDate:[NSDate date]];
an NSString (or any object, for that matter) created with a class method, and not an init method, will be autoreleased. This means on the next iteration of the event loop, current_Test_id is released, and now you have a pointer to a dead object.
See this similar question
As current_Test_id is instance method.
in the init (in case of mac os) or viewDidLoad (for ios) alloc+init it.
and then assign :
current_Test_id=[NSString stringWithString:unique_id]; //it will be in autorelease mode.
or
current_Test_id=[[NSString stringWithString:unique_id]retain];

Returning a string

I have just started with Objective-C and have a (probably) very, very basic question/problem.
int testf(int x){
NSDateFormatter *dateformatter=[[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"dd.MM.yyyy"];
NSString *infstr=[dateFormatter stringFromDate:[[NSDate] dateByAddingTimeInterval:(60*x)];
return infstr;
}
NSString *testString=testf(1);
I currently have the following problem: I don't know how to return a string from a function. I couldn't even figure it out through Google.
The aforementioned Code leads to a warning "Pointer from integer without a cast". I hope anyone does know a solution to this "problem" and is willing to share it with me.
Thanks in advance.
You need to create an NSString object using the int.
NSString *testf(int x) {
return [NSString stringWithFormat:#"%i",x];
}
NSString *testString = testf(1);
Just change your return type from int to an NSString pointer like so:
NSString* testf(int x){
NSDateFormatter *dateformatter=[[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"dd.MM.yyyy"];
NSString *infstr=[dateFormatter stringFromDate:[NSDate dateByAddingTimeInterval:(60*x)]];
[dateFormatter release];
return infstr;
}
NSString *testString=testf(1);
Also your dateFormatter is leaking, so add the release statement as shown above.

Getting date info into an array in Objective-C Cocoa framework

I have a function that returns an array to hold date info.
- (NSArray*) getTodayArray
{
NSDate *today = [NSDate date];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"YYYY"];
NSString *year = [dateFormatter stringFromDate:today];
[dateFormatter setDateFormat:#"MM"];
NSString *month = [dateFormatter stringFromDate:today];
[dateFormatter release];
NSArray *res = [NSArray arrayWithObjects: year, month, nil];
return res;
}
Q1 : Is there any easy way to get all the info (year, month, date, hour, minute ...) in an array not using setDateFormat over and over again?
Q2 : Is there a way so that I can access the content of array using res['year'] or similar? I mean using dictionary?
Q3 : Do I need to release NSArray *res in the caller of this function?
A1: You can do smth like this:
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"YYYY|MM"];
NSArray* d_arr = [[dateFormatter stringFromDate: [NSDate date]] componentsSeparatedByString: #"|"];
A2: Use NSDictionary:
[NSDictionary dictionaryWithObjectsAndKeys: [d_arr objectAtIndex: 0], #"year", [d_arr objectAtIndex: 1], #"month", nil]
A3: return value is autoreleased. you don't need to release it.
#prosseek
1 - I dont think you have another choice to get the year, month, date, hour, minute ... from NSDate other than this.(I am not sure about it though.)
2 - you can access the objects in the dictionary in the above format but something more like objective-c style. like this
[dateDictionary obectForKey:#"year"];
but you need to define the dictionary in that format
like this
NSDictionary *dateDictionary = [NSDictionary dictionaryWithObjects:year,min,hr,nil forKeys:#"year", #"min", #"hour", nil];
3 - no you dont need to release or autorelease the NSArray in the above method . but i think you need to retain it in the array that is receiving res array if you want to use it after a while.
Why don't you just use a NSArray of NSDates?
You can probably get all of your desired functionality out of its plethora of functions.
A1: You could dump it all out into a string, but then you'd have to parse the string, which wouldn't be any easier.
A2: You could do that if you used an NSDictionary instead of an NSArray.*
A3: No, it's already autoreleased.
* Why don't you write a category for NSDate instead?
NSDate+Convenience.h
#interface NSDate (Convenience)
- (NSInteger)year;
- (NSInteger)month;
#end
NSDate+Convenience.m
#implementation NSDate (Convenience)
- (NSInteger)year {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"YYYY"];
NSString *myYear = [dateFormatter stringFromDate:self];
[dateFormatter release];
return myYear;
}
- (NSInteger)month {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"MM"];
NSString *myMonth = [dateFormatter stringFromDate:self];
[dateFormatter release];
return myMonth;
}
#end
Just #include NSDate+Convenience.h wherever you want to use your handy date and month accessors. All of your NSDate instances will then get them:
NSDate *myDate = [NSDate date];
NSLog(#"%ld %ld", [myDate year], [myDate month]);
No need for loosely-typed NSArrays or NSDictionaries to store this stuff.
(Note you could modify the above code to use a shared NSDateFormatter.)
Q1: Not an array, but you can use -[NSCalendar components:fromDate:] to get an NSDateComponents object. You can use it directly or build an array from it, if that is your preference.
Q2: No, but if you return an NSDateComponents object, then you can use -year, -month, etc methods on it.
Q3: No, you don't need to release it in this method or the caller, unless the caller retains it (which may be desirable).
You're looking for the NSDateComponents class. You'll need to create an NSCalendar object first, then call the components:fromDate: method to get the DateComponents object, after which you can access the object's month, year etc. properties.
Not quite sure what you want here. As it stands, the array cannot be accessed in the manner you describe, though if you want you could always create a dictionary and assign values for keys such as 'month' or 'year'. However, it might just be easier to return the DateComponents object, and access its properties.
No, there is no need to release the NSArray. You constructed it using the NSArray class method, which is already autoreleased.

iPhone simple method definition and calling the current date/time

I'm very new to iPhone development, and I'm trying to write a function which will accept one parameter, and return the current date/month and store it in a variable.
But I'm getting a (null) value with NSLog.
Method:
-(NSString *) getNowDateMonth:(NSString *)type {
NSDate *now = [[NSDate alloc] init];
if (type==#"month") {
NSDateFormatter *monthFormat = [[NSDateFormatter alloc] init];
[monthFormat setDateFormat:#"MM"];
NSString *theMonth = [monthFormat stringFromDate:now];
[monthFormat release];
return theMonth;
} else if (type==#"day") {
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:#"dd"];
NSString *theDate = [dateFormat stringFromDate:now];
//int setDate = theDate;
[dateFormat release];
return theDate;
}
[now release];
return NULL;
}
Calling the function to get value:
NSString *month = [self getNowDateMonth:#"month"];
NSLog(#"%#", month);
Am I going about this the right way?
First of all, compare the strings using [#"month" isEqualToString:type], because two strings containing the same text ("month") may not be equal by the == operator. == checks if they're the same string object, not strings object with the same contents.
Second of all, you're leaking the date when returning the month or day (not releasing now). You should use [NSDate date]; instead of [[NSDate alloc] init].
To sum up, a suggested better version of this method would be:
-(NSString *) getNowDateMonth:(NSString *)type {
NSDate *now = [NSDate date];
if ([#"month" isEqualToString:type]) {
NSDateFormatter *monthFormat = [[NSDateFormatter alloc] init];
[monthFormat setDateFormat:#"MM"];
NSString *theMonth = [monthFormat stringFromDate:now];
[monthFormat release];
return theMonth;
} else if ([#"day" isEqualToString:type]) {
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:#"dd"];
NSString *theDate = [dateFormat stringFromDate:now];
[dateFormat release];
return theDate;
} else {
return nil;
}
}
Also, there are a few other points that can be taken into consideration to improve this method:
do not use NSString as type; use an enum
do not allocate NSDateFormatter on each call to the method; instead use a static variable in the method
You want to use NSDateComponents to reliably and easily extract unit information i.e. month, day, week etc from an NSDate.
See Date and Time Programming Guide for Cocoa.
Dates are a deceptively complex programing problem so Cocoa has a fully developed set of classes for dealing with them. However, the learning curve is a bit steep.