Writing our first unit test by testing a person

The iOS platform hasn't always been the most unit test focused but since Xcode 4 this has changed - with a lot of the set up work taken care of for you. Making adding unit tests to your application easier than ever, however the question still remains what should we test. Below we've got an existing method that we are going to write some unit tests against. So let's have a look at it:

- (id)initWithFirstname:(NSString *)firstname surname:(NSString *)surname age:(NSInteger)age sex:(UTEPersonSex)sex
{
     self = [super init];

     if (self) {

          if ([firstname length] == 0) {

                NSLog(@"firstname is nil...");

                return nil;

          }

          if ([surname length] == 0) {

                NSLog(@"surname is nil...");

                return nil;

          }

          if (age < 0) {

              NSLog(@"age is 0...");

              return nil;

          }

          if (sex != PersonSexMale && sex != PersonSexFemale) {

              NSLog(@"sex is nil...");

              return nil;

          }

          firstname_ = firstname;
          surname_ = surname;
          age_ = age;
          sex_ = sex;

      }

       return self;
}

In the above method we have the UTEPerson class's designated initialiser. By looking at the structure of the method we can see that it had a number of different branches within it, each branch containing a different path through the method. With unit testing it's purpose to only focus on the successful or happy paths through a method however I also want you think about the unsuccessful or unhappy paths as well.

Ok now that we've had a think about it, let's list those paths:

  1. Create a valid person and check person is non nil
  2. Create a valid person and check firstname assignment
  3. Create a valid person and check surname assignment
  4. Create a valid person and check age assignment
  5. Create a valid person with age equaling zero and check age assignment
  6. Create a valid person with sex equaling male and check sex assignment
  7. Create a valid person with sex equaling female and check sex assignment
  8. Zero length for the firstname parameter
  9. Nil for firstname parameter
  10. Zero length for the surname parameter
  11. Nil for surname parameter
  12. Minus value for age parameter
  13. Invalid value for sex

If you speak to unit test purist they will tell you that rule is “one unit test, one assert”. While this has merit I believe that the rule should be to ensure that our unit tests have cohesion. If you want to have more than one assert in your unit test you need to ensure that the unit test still has one purpose.

So let's implement the above paths into unit tests.

- (void)testInitializationSuccessfulWithValidPersonBeingReturned
{
       UTEPerson *person = [[UTEPerson alloc] initWithFirstname:@"johnny" surname:@"appleseed" age:26 sex:PersonSexMale];

       STAssertNotNil(person, @"This person object should not be nil");
}

Woohoo! First unit test, out of the way!

I always like to start with happy paths test cases as its good for my morale and helps to avoid the scenario were if you start with an unhappy path its possible that the unit test fails because your method is broken rather than for the purpose that you think that you are testing against.

Let's look into the structure of the unit test and determine what is actually doing. The assert STAssertNotNil is used when we want to check that an object is not nil, it takes the object under test and a description. The description parameter is printed out to the console upon failure and should describe what has caused the failure in a human readable manner. It can take a variable set of parameters in the same manner that stringWithFormat does.

Let’s crack on and complete the other unit tests.

I'll wait while you complete them.

By the end your UTEPersonTest.m should contain these methods (or something similar to them):

- (void)testInitializationSuccessfulWithFirstnameParameterBeingCorrectlyAssigned
{
         NSString *firstname = @"johnny";
         UTEPerson *person = [[UTEPerson alloc] initWithFirstname:firstname surname:@"appleseed" age:26 sex:PersonSexMale];

         STAssertEqualObjects(firstname, person.firstname, @"Person's firstname should be: %@ instead it is: %@", firstname, person.firstname);
}

- (void)testInitializationSuccessfulWithSurnameParameterBeingCorrectlyAssigned
{
         NSString *surname = @"appleseed";
         UTEPerson *person = [[UTEPerson alloc] initWithFirstname:@"johnny" surname:surname age:26 sex:PersonSexMale];

        STAssertEqualObjects(surname, person.surname, @"Person's surname should be: %@ instead it is: %@", surname, person.surname);
}

- (void)testInitializationSuccessfulWithAgeParameterBeingCorrectlyAssigned
{
         NSInteger age = 26;
         UTEPerson *person = [[UTEPerson alloc] initWithFirstname:@"johnny" surname:@"appleseed" age:age sex:PersonSexMale];

         STAssertEquals(age, person.age, @"Person's age should be: %d instead it is: %d", age, person.age);
}

- (void)testInitializationSuccessfulWithAgeParameterBeingZeroAndCorrectlyAssigned
{
        NSInteger age = 0;
        UTEPerson *person = [[UTEPerson alloc] initWithFirstname:@"johnny" surname:@"appleseed" age:age sex:PersonSexMale];

        STAssertEquals(age, person.age, @"Person's age should be: %d instead it is: %d", age, person.age);
}

- (void)testInitializationSuccessfulWithSexParameterBeingCorrectlyAssigned
{
        PersonSex sex = PersonSexMale;
        UTEPerson *person = [[UTEPerson alloc] initWithFirstname:@"johnny" surname:@"appleseed" age:sex sex:sex];

        STAssertEquals(sex, person.sex, @"Person's sex should be: %d instead it is: %d", sex, person.sex);

}

- (void)testInitializationUnsuccessfulWithFirstnameBeingNil

{
       UTEPerson *person = [[UTEPerson alloc] initWithFirstname:nil surname:@"appleseed" age:26 sex:PersonSexMale];

       STAssertNil(person, @"This person object should be nil");
}

- (void)testInitializationUnsuccessfulWithFirstnameBeingOfZeroLength
{
       UTEPerson *person = [[UTEPerson alloc] initWithFirstname:@"" surname:@"appleseed" age:26 sex:PersonSexMale];

       STAssertNil(person, @"This person object should be nil");
}

- (void)testInitializationUnsuccessfulWithSurnameBeingNil
{
        UTEPerson *person = [[UTEPerson alloc] initWithFirstname:@"johnny" surname:nil age:26 sex:PersonSexMale];

        STAssertNil(person, @"This person object should be nil");


- (void)testInitializationUnsuccessfulWithSurnameBeingOfZeroLength
{
        UTEPerson *person = [[UTEPerson alloc] initWithFirstname:@"johnny" surname:@"" age:26 sex:PersonSexMale];

        STAssertNil(person, @"This person object should be nil");
}

- (void)testInitializationUnsuccessfulWithAgeBeingANegativeValue
{
         UTEPerson *person = [[UTEPerson alloc] initWithFirstname:@"johnny" surname:@"appleseed" age:-26 sex:PersonSexMale];

         STAssertNil(person, @"This person object should be nil");
}

- (void)testInitializationUnsuccessfulWithSexBeingNeitherMaleNorFemale
{
        UTEPerson *person = [[UTEPerson alloc] initWithFirstname:@"johnny" surname:@"appleseed" age:26 sex:34];

        STAssertNil(person, @"This person object should be nil");
}

Its important to note that in the above happy paths we are not testing to see that the property/synthesize works properly rather what we are really testing is that the variables that are passed in as parameter are being correctly assigned to instance variables. If you ever find yourself in a situation were you are testing framework or third-party libraries then your test case is invalid.

Ok so we’ve taken care of the initialiser method for UTEPerson but I think that we now have room to improve our actual test suite because I see a lot of duplicated code in our example test cases. We should abstract the common values (@"johnny", @"appleseed", etc) out of the individual unit tests and use properties set to these values.