0

I have a unit test:

- (void)testFetchTrackByTrackIdIsATrack
{
    [self addTrackWithSongId:@"123"];
    Track *fetchedTrack = [self.library trackByTrackId:@"123"];
    assertThat(fetchedTrack, instanceOf([Track class]));
}

Which fails with:

file:///Users/elliot/Development/Hark/HarkTests/TestLibrary.m: test failure:
-[TestLibrary testFetchTrackByTrackIdIsATrack] failed: Expected an instance
of Track, but was Track instance <Track: 0x6180001077d0>

I have several other tests that use the same instance checking on different classes that work - but I can't workout why this doesn't work. Delving deeper:

- (void)testFetchTrackByTrackIdIsATrack
{
    [self addTrackWithSongId:@"123"];
    Track *fetchedTrack = [self.library trackByTrackId:@"123"];
    Class c1 = [fetchedTrack class];
    Class c2 = [Track class];
}

Debugger reports:

c1  Class   Track           0x0000000100012fe0
c2  Class   0x1000b3eb8     0x00000001000b3eb8

Notice how it can't see that [Track class] is a class of type Track? When I apply this same logic to other unit tests that are passing they both report the correct class name.

It feels like it doesn't have the class metadata at runtime, but why?

Some more cases:

assertTrue(c1 == c2);                                   // FAIL
assertThat([fetchedTrack classDescription],
  equalTo([Track classDescription]));                   // PASS
assertTrue([fetchedTrack isKindOfClass:[Track class]]); // FAIL
Elliot Chance
  • 5,526
  • 10
  • 49
  • 80

1 Answers1

5

There are any number of reasons why the class's address may not be constant over time and most of them boil down to the use of Key-Value Observing.

When an instance is observed, the runtime (the Foundation framework, really) composes a new class that is a subclass of the instance's original class that contains the observation mechanisms.

This results in the inability to use pointer comparison. It also means that certain types of introspection of the class will fail to yield expected results.

In short; use the isKindOfClass: and isMemberOfClass: methods for all such tests and never use pointer equality testing (for this or for comparing instances, in general).

bbum
  • 162,346
  • 23
  • 271
  • 359
  • reference: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOImplementation.html – vikingosegundo Mar 12 '14 at 22:07
  • 1
    Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data. When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. – vikingosegundo Mar 12 '14 at 22:07
  • The `instanceOf()` matcher in OCHamcrest uses `isKindOfClass:`. I have added the explicit test to my original post to show that it still doesn't work... – Elliot Chance Mar 13 '14 at 10:06
  • @bbum But isn't the KVO subclass implementation of `class` return `[super class]` to attempt to hide itself? In that case, why wouldn't the two class pointers returned be the same? – Léo Natan Mar 13 '14 at 22:26
  • @LeoNatan It isn't that simple; there could be any number of swizzles in between. And there are multiple ways to retrieve the `isa`. All of which is why one should use the API that is intended to query this exact bit of information. – bbum Mar 13 '14 at 23:05
  • @bbum I am no disputing correct API use, just interested why a pointer would change. The entire swizzle chain should have maintained the original "`super`" value. Meanwhile, if someone did a careless swizzle, perhaps it is for the best not to pass the equality test - but then again, the usefulness of such requirement of a specific `Class` eludes me. – Léo Natan Mar 13 '14 at 23:10