4
\$\begingroup\$

I have known about unit testing for a while now, but I am just now finally understanding how to implement it. I think that my initial implementation is a little rough so I could use some feedback.

I have implemented these tests in order to verify that a new feature of my strategy game is working properly. Previously, I would have output these values to the screen somewhere and simply watched to make sure that the expected behavior was happening. With these tests, I have concrete proof that the feature is working. This is much better!

However there are some problems. I needed to implement a Testing category for the class that I am testing because some of the methods I needed to run the tests are private. By creating this category at the top of the testing class, I can use private methods from the class without having to permanently expose them. This works, but I have heard that I should only be unit testing public methods, so I am wondering if what I have done is acceptable. I should note that I also had to add a couple getter methods to the class to get some information that was not public.

DTDwarfTests.m

#import <XCTest/XCTest.h>
#import "DTDwarf.h"
#import "DTJobUnit.h"

@interface DTDwarf (Testing)
    -(void) setMoodForTesting:(int)moodAmount;
    -(void) startJobCountdown:(JobType)jobType;
    -(BOOL) doJobCountdown;
    -(void) calculateMoodState;
    -(void) calculateMoodAmountForJob;
    -(JobType) getFavoriteJob;
    -(JobType) getHatedJob;
@end

@interface DTDwarfTests : XCTestCase
@end

@implementation DTDwarfTests

- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
}
- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

-(void) testMoodInitialization {
    DTDwarf *dwarf = [[DTDwarf alloc]initWithWorldSize:CGSizeZero];
    XCTAssert(dwarf.moodAmount > 0);
}
-(void) testFavoriteAndHatedJobsValues {
    DTDwarf *dwarf = [[DTDwarf alloc]initWithWorldSize:CGSizeZero];
    XCTAssert([dwarf getFavoriteJob] != JobTypeNumJobTypes);
    XCTAssert([dwarf getHatedJob] != JobTypeNumJobTypes);
}
-(void) testFavoriteJobMoodInfluence {
    DTDwarf *dwarf = [[DTDwarf alloc]initWithWorldSize:CGSizeZero];
    [dwarf setMoodForTesting:0];
    dwarf.dwarfJobUnit = [[DTJobUnit alloc]initWithJobType:[dwarf getFavoriteJob]];
    [dwarf calculateMoodAmountForJob];
    XCTAssert(dwarf.moodAmount > 0);
}
-(void) testHatedJobMoodInfluence {
    DTDwarf *dwarf = [[DTDwarf alloc]initWithWorldSize:CGSizeZero];
    [dwarf setMoodForTesting:1000];
    dwarf.dwarfJobUnit = [[DTJobUnit alloc]initWithJobType:[dwarf getHatedJob]];
    [dwarf calculateMoodAmountForJob];
    XCTAssert(dwarf.moodAmount < 1000);
}
-(void) testBadMoodIncreasesCountdown {

    DTDwarf *happyDwarf = [[DTDwarf alloc]initWithWorldSize:CGSizeZero];
    [happyDwarf setMoodForTesting:1000];
    [happyDwarf calculateMoodState];
    [happyDwarf startJobCountdown:JobTypeMining];
    int happyDwarfCountdown = 0;
    while (![happyDwarf doJobCountdown]) {
        happyDwarfCountdown++;
    }

    DTDwarf *sadDwarf = [[DTDwarf alloc]initWithWorldSize:CGSizeZero];
    [sadDwarf setMoodForTesting:0];
    [sadDwarf calculateMoodState];
    [sadDwarf startJobCountdown:JobTypeMining];
    int sadDwarfCountdown = 0;
    while (![sadDwarf doJobCountdown]) {
        sadDwarfCountdown++;
    }

    XCTAssert(happyDwarfCountdown < sadDwarfCountdown);
}

@end
\$\endgroup\$
2
\$\begingroup\$

Unit testing isn't something I do a whole lot of. So as for a general review of what you've written, I can't attest much to that.

But as to the idea of creating a class category just to sorta-kinda exposed "private" methods for the sake of testing and testing only? Well... let's talk about Objective-C, shall we?

Despite the way we talk about "methods", Objective-C doesn't really do... methods... they're all messages which invoke underlying C functions.

In other languages, if you try to call a non-existent method, your code won't compile. In Objective-C, you'll merely get a warning if the file you're in can't see the method you're trying to call, and if it really doesn't exist, you'll get a runtime crash.

But the fact remains that you can still compile the code and you can still pass the message to the object. And if the object implements the method that matches the message, it will execute the code.

So even without the class category, we could still call the methods on the object--we'd just have a bunch of warnings.

And there's ways around the warnings as well. This blog discusses this in more detail, but here's an example of doing this with one of your methods:

Given the method:

- (void)setMoodForTesting:(int)moodAmount;

We could invoke this method as such:

SEL mftSelector = NSSelectorForString(@"setMoodForTesting:");

((void (*) (id, SEL, float))objc_msgSend)(dwarf, mftSelector,1000);

It may also be worth looking into this blog entry for some information on cleaning this up and making it more reasonable to use.

In the end, this allows you to still test private details of the class, but these details aren't exposed anywhere to anyone. If somehow you were to accidentally import this .m directly, you would then expose these private details and could accidentally use them somewhere.

\$\endgroup\$
1
\$\begingroup\$

I don't agree that unit testing should apply only to public methods. Decomposing public methods into more restricted pieces is not just useful design, but is often critical to testability. Further, unit testing is, by definition, not black-box testing; it can and should be aware of implementation details. It's important, for example, to test internal boundary conditions, even when those boundaries are invisible to the caller.

\$\endgroup\$
2
  • \$\begingroup\$ This isn't really an answer. It's more of a comment. \$\endgroup\$ – nhgrif Nov 12 '14 at 2:27
  • \$\begingroup\$ It's both. @bazola asked if it is acceptable to open up private methods so they can be tested. My answer addressed this question. \$\endgroup\$ – schnitz Nov 12 '14 at 5:58
1
\$\begingroup\$

When the entirety of the contents of a method is nothing but:

[super doMethod];

You can simply delete the entire method from your source code. Otherwise, it's just clutter.

There is only one reason to include an empty (besides super) method in your source code, and that's for this trick.

BUT... I wouldn't recommend you actually delete these methods.

I recommend we move all of our repeated code we need to do every time into the setUp method.

We can make a property for a dwarf and initialize it in setUp. In this case, it's only a single line in the other methods. But it's good practice, and in future cases you might want to do more setup.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.