3

I would like to test the deserialization of my object basket of type Basket which contains an array of objects of typeAProduct.

All of my product classes inherit from AProduct which is an abstract class.

Gradually, I will have more and more product classes with, for each, different attributes.

Currently, that is how I test the deserialization of my Basket:

Basket *oldBasket = BasketInstance;

[BasketInstance resetBasket]; // BasketInstance is a macro which points on the current Basket in singleton.

[BasketInstance deserializeItself:[self getBasketSerializedForTest]]; // getBasketSerializedForTest return a dictionnary of the same basket serialized.

XCTAssertTrue([BasketInstance isEqual:oldBasket], @"Basket deserialization is no correct");

The main problem with this implementation is that I have to override the isEqual method in Basket class and for each AProduct object, override isEqual method to check all of the attributes. I think this is very dangerous because if me or an other developer adds a new attribute and forgets to check the values of this attribute on the isEqual method, the unit test of the basket could succeed with a broken deserilization.

Is there any solution to avoid this risk ? I though parse all of the properties of the class and for each attribute, run isEqualmethod, but I wish there is a much better solution.

Thanks

Jean Lebrument
  • 5,079
  • 8
  • 33
  • 67

2 Answers2

3

The standard OOP approach would be that each subclass test for its own attributes. So each attribute will be checked once. I think it is straight forward and an easy task. No danger at all and less complicated than juggling with the runtime.

@interface AProduct : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *stockIdentifier;
@end

@implementation AProduct
-(BOOL)isEqual:(id)object
{
    BOOL equal= [object isKindOfClass:[self class]]
                && [[(AProduct *)object name] isEqualToString:self.name]
                && [[(AProduct *)object stockIdentifier] isEqualToString:self.stockIdentifier];
    return equal;
}
@end


@interface Shirt : AProduct
@property (nonatomic, copy) NSString *size;
@property (nonatomic, copy) NSString *color;
@end


@implementation Shirt
-(BOOL)isEqual:(id)object
{
    BOOL equal = [super isEqual:object];
    if (equal) {
        equal = [[(Shirt *)object size] isEqualToString:self.size]
                && [[(Shirt *)object color] isEqualToString:self.color];
    }

    return equal;
}

and as you are concerned about unit testing: you should write an test for equality that i suppose to fail as-well. if than a red and blue shirt do evaluate as equal, this test fails and you know that you have to look at -isEqual:


lets say I am just subclassing AProduct as Shirt. AProduct has it's -isEqual: working, but I didn't overwrite and extend it in Shirt yet.

Now I write following test method:

- (void)testShirtInEquality
{
    Shirt *redShirt = [[Shirt alloc] init];
    redShirt.name =@"Shirt";
    redShirt.stockIdentifier =@"shirt";
    redShirt.size = @"XXL";
    redShirt.color = @"red";

    Shirt *blueShirt = [[Shirt alloc] init];
    blueShirt.name =@"Shirt";
    blueShirt.stockIdentifier =@"shirt";
    blueShirt.size = @"XXL";
    blueShirt.color = @"blue";
    XCTAssertNotEqualObjects(redShirt, blueShirt, @"not equal!");
}

It will fail, as the AProduct implementation of -isEqual: will trigger and find everything to be the same. Now I know I have to write the code to make this test pass, and I add

-(BOOL)isEqual:(id)object
{
    BOOL equal = [super isEqual:object];
    if (equal) {
        equal = [[(Shirt *)object size] isEqualToString:self.size]
        && [[(Shirt *)object color] isEqualToString:self.color];
    }

    return equal;
}

The test passes.

So actually the answer is not to write a dynamic -isEqual: method, but to stick closely to test driven development.


Just to recap:

TDD Waltz

  • write a single failing test
  • write simplest code to pass the test
  • refactor production and test code
  • and repeat
vikingosegundo
  • 52,040
  • 14
  • 137
  • 178
  • Yes thanks, I'm agree with you. But it's a forbidding task to add a comparing tests in isEqual method (or a custom method which doing the same thing) to every attribute I add into the class. But when I see all of your answers, I'm thinking there is no other way. – Jean Lebrument Apr 04 '14 at 13:36
  • why is it forbidden to add a comparison test to `-isEqual:`? – vikingosegundo Apr 04 '14 at 13:38
  • We can easily forget to add a comparison and then, the unit test will not check completely the equality between two objects. – Jean Lebrument Apr 04 '14 at 13:40
  • and that is why you do unit testing: to get reminded. that is also why you should first write test that will fail, and than turn them green by adding the right code. – vikingosegundo Apr 04 '14 at 13:41
  • (I think @JeanLbr means "foreboding".) – Wevah Apr 04 '14 at 14:04
  • @vikingosegundo Yes, you are absolutely right ! Your argument sounds right ! – Jean Lebrument Apr 04 '14 at 14:50
1

If you want to have automatic compare method for unit test purposes and this method should be resistant for changes in list of properties, then there is no choice you have to do it using meta data.
I would not do it using isEqual: method in production code since such automatic method could confuse someone and lead to unexpected problems. So comparator should be available only for unit test purposes.

BOOL areEqual(AProduct *a, AProduct *b) {
    if (![a isMemberOfClass: [b class])
        return NO;

    unsigned int count = 0, i;
    objc_property_t *props = class_copyPropertyList([b class], &count)
    for (i=0; i<count; ++i) {
        objc_property_t property = props[i];
        NSString *name = [NSString stringWithUTF8String:property_getName(property)];

        id aValue = [a valueForKey: name];
        id bValue = [b valueForKey: name];

        if ([aValue isKindOfClass: [AProduct class]) {
            if (!areEqual((AProduct *)aValue, (AProduct *)bValue))
                return NO;
        } else {
            if (![aValue isEqual: bValue])
                return NO;
        }
    }
    return YES;
}

Probably I've forgot about something but you should have general picture how you can do it.

Marek R
  • 32,568
  • 6
  • 55
  • 140
  • Thanks for your answer ! I think this method is quite dangerous because if a developer add an attribute to a class without check it in areEqual method, the unit tests for the basket will no longer be useful. But if there is no other method, I will do this. – Jean Lebrument Apr 04 '14 at 12:21