Excellent question. It’s a subtle difference, but in your example you’re not changing the object that testString points to, you’re changing where testString points.
If we assume for a moment that there’s no such thing as ARC, or garbage collection, after your code snippet there is still a lonely NSString object representing “hello there” floating in memory somewhere, never to be seen again, because you’ve taken away the only way for the code to interact with it. You’ve changed the pointer to point to a different NSString object, one that says “hello there again”.
C does have a way to declare pointers such that they cannot be redirected like this, using the const keyword. If you place it in the right location in your declaration, you can prevent the reassignment:
NSString * const testString = @"hello there";
testString =@"hello there again!"; // The compiler will throw an error here
I’m not sure whether Objective-C has another way to do this, but since it’s a superset of C, the above works fine.