There are three related mistakes contributing to this bug:
1. The test is depending on dealloc being done eagerly.
2. The Attempt class puts unsubscribe code in its dealloc method.
3. The Attempt class does not force callers to control its lifetime.
I'll go into more depth on these.
1. Cleanup is often deferred. Dealloc may only run AFTER the test instead of DURING the loop (e.g. I think NSArray defers releasing its items in some cases). Tests that depend on cleanup occurring eagerly, without explicitly waiting or forcing it, are brittle. Given that Attempt uses dealloc to unsubscribe, and we are testing that it unsubscribed, we should at the very least allocate it in an autoreleasepool that ends before we test that it unsubscribed.
2. Putting unsubscribe code in dealloc is a great way to upgrade minor memory leak bugs into major behavior bugs. The article gives fantastic examples of this happening many different ways. Don't rely on the object happening to go out of scope at the right time. If your object's lifetime is controlling whether or not side effects occur, that's a bad situation to be in.
3. Someone has to be responsible for when unsubscribing happens (since we took it away from dealloc), and the someone most in a position to know when is the caller. We should force them to tell us our subscription lifetime. I like to control lifetimes with cancellation tokens [1], so I'd write the attempt class like this:
I disagree with your mistake #1, and therefore also with the second one. In ARC, dealloc is done eagerly. So, I don't have a problem with unsubscribing in dealloc.
I dislike that receiving the notification mutates global state though. I can't clearly articulate it, but that's why I think the caller should have explicit control over when the behavior is active. I think notification subscriptions that last for the lifetime of the object should probably be related to the object's internal state.
Finally, I went to the clang docs regarding timing of dealloc, and as I read it, they shoot for immediate deallocation, but let it slide for local variables:
Precise lifetime semantics
In general, ARC maintains an invariant that a retainable object pointer
held in a __strong object will be retained for the full formal lifetime
of the object. Objects subject to this invariant have precise
lifetime semantics.
By default, local variables of automatic storage duration do
not have precise lifetime semantics. Such objects are simply strong
references which hold values of retainable object pointer type,
and these values are still fully subject to the optimizations
on values under local control.
Rationale
Applying these precise-lifetime semantics strictly would be prohibitive.
Many useful optimizations that might theoretically decrease the
lifetime of an object would be rendered impossible. Essentially,
it promises too much.
A local variable of retainable object owner type and automatic storage
duration may be annotated with the objc_precise_lifetime attribute to
indicate that it should be considered to be an object with precise lifetime
semantics.
Rationale
Nonetheless, it is sometimes useful to be able to force an object to
be released at a precise time, even if that object does not appear
to be used. This is likely to be uncommon enough that the syntactic
weight of explicitly requesting these semantics will not be burdensome,
and may even make the code clearer.
1. The test is depending on dealloc being done eagerly.
2. The Attempt class puts unsubscribe code in its dealloc method.
3. The Attempt class does not force callers to control its lifetime.
I'll go into more depth on these.
1. Cleanup is often deferred. Dealloc may only run AFTER the test instead of DURING the loop (e.g. I think NSArray defers releasing its items in some cases). Tests that depend on cleanup occurring eagerly, without explicitly waiting or forcing it, are brittle. Given that Attempt uses dealloc to unsubscribe, and we are testing that it unsubscribed, we should at the very least allocate it in an autoreleasepool that ends before we test that it unsubscribed.
2. Putting unsubscribe code in dealloc is a great way to upgrade minor memory leak bugs into major behavior bugs. The article gives fantastic examples of this happening many different ways. Don't rely on the object happening to go out of scope at the right time. If your object's lifetime is controlling whether or not side effects occur, that's a bad situation to be in.
3. Someone has to be responsible for when unsubscribing happens (since we took it away from dealloc), and the someone most in a position to know when is the caller. We should force them to tell us our subscription lifetime. I like to control lifetimes with cancellation tokens [1], so I'd write the attempt class like this:
Here's what the test should look like: Hopefully I got that right. I don't have a mac nearby to test it out at the moment, and I've never used NSNotificationCenter before.1: http://twistedoakstudios.com/blog/Post7391_cancellation-toke...