Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to unit test a UIViewController - TDD/BDD

Unit testing is just something I never seem to be able to get my head around but I can see why its important and can be a huge time saver (if you know what you're doing). I am hoping that someone can point me in the right direction.

I have the following UIViewController

QBElectricityBaseVC.h

@interface QBElectricityBaseVC : QBStateVC

@property (nonatomic, strong) QBElectricityUsage *electricityUsage;
@property (nonatomic, assign) CGFloat tabBarHeight;

- (void)updateElectricityUsage;

@end

QBElectricityBaseVC.m

@implementation QBElectricityBaseVC

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.tabBarItem = [[UITabBarItem alloc] initWithTitle:NSLocalizedString(@"electricity_title", nil) image:nil tag:0];
    }
    return self;
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    [self.notificationCenter addObserver:self selector:@selector(updateElectricityUsage)
                                                 name:kUpdatedElectricityUsageKey object:nil];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];

    [self.notificationCenter removeObserver:self];
}

- (void)updateElectricityUsage
{
    self.electricityUsage = [self.stateManager electricityUsage];
}

- (CGFloat)tabBarHeight
{
    return self.tabBarController.tabBar.frame.size.height;
}

@end

What should I test?

  • An observer for kUpdatedElectricityUsageKey is added
  • self.electricityUsage becomes an instance of QBElectricityUsage
  • A tabBarHeight is returned
  • An observer for kUpdatedElectricityUsageKey is removed

Am I missing anything I should test or testing something I really shouldn't?

How do I test?

So I am trying to do this using Specta and Expexta. If I need to mock anything I would be using OCMockito.

I really don't know how to test the observer is added/removed. I see the following in the Expexta documentation but not sure if its relevant/how to use it:

expect(^{ /* code */ }).to.notify(@"NotificationName"); passes if a given block of code generates an NSNotification named NotificationName.

expect(^{ /* code */ }).to.notify(notification); passes if a given block of code generates an NSNotification equal to the passed notification.

To test that self.electricityUsage becomes an instance of QBElectricityUsage I could create a category that has a method that just pretends the notification fired and calls the updateElectricityUsage method but is this the best way?

And as for the tabBarHeight, should I just test that it returns a valid CGFloat and not worry what the value is?


UPDATE

I changed my viewWillAppear method to look like below:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self addNotificationObservers];
}

- (void)addNotificationObservers
{
    [self.notificationCenter addObserver:self selector:@selector(updateElectricityUsage)
                                    name:kUpdatedElectricityUsageKey object:nil];
}

And then I created the following test:

#import "Specs.h"

#import "QBElectricityBaseVC.h"
#import "ElectricityConstants.h"

SpecBegin(QBElectricityBaseVCSpec)

    describe(@"QBElectricityBaseVC", ^{
        __block QBElectricityBaseVC *electricityBaseVC;
        __block NSNotificationCenter *mockNotificationCenter;

        beforeEach(^{
            electricityBaseVC = [QBElectricityBaseVC new];
            mockNotificationCenter = mock([NSNotificationCenter class]);
            electricityBaseVC.notificationCenter = mockNotificationCenter;
        });

        afterEach(^{
            electricityBaseVC = nil;
            mockNotificationCenter = nil;
        });

        it(@"should have a notification observer for updated electricity usage", ^{
            [electricityBaseVC addNotificationObservers];
            [verify(mockNotificationCenter) addObserver:electricityBaseVC selector:@selector(updateElectricityUsage)
                                               name:kUpdatedElectricityUsageKey object:nil];
        });
    });

SpecEnd

That test now passes but is this the correct/best way to test this?

like image 547
Hodson Avatar asked Nov 27 '14 14:11

Hodson


2 Answers

You've just felt one big con of iOS ViewControllers: they suck at testability.

  • ViewControllers mix logic of managing the view and model
    • This leads to massive ViewControllers
    • This violates the Single Responsibility Rule
      • This makes code not reusable

Another big problem with MVC is that it discourages developers from writing unit tests. Since view controllers mix view manipulation logic with business logic, separating out those components for the sake of unit testing becomes a herculean task. A task that many ignore in favour of… just not testing anything.

Article - source

Maybe you should think about using MVVM instead. This is a great article explaining the difference of iOS MVC and MVVM.

The great thing about using MVVM is that you can use DataBinding using Reactive Cocoa. Here's a tutorial that will explain Data Binding with MVVM and reactive programming in iOS.

like image 58
michal.ciurus Avatar answered Oct 03 '22 05:10

michal.ciurus


I follow 2 practices for testing the pieces of a UIViewController.

  1. MVVM - with an MVVM pattern you can very easily unit test the content of your views in your unit tests for your ViewModel classes. This also keeps your ViewController logic very light so you don't have to write as many UI tests to cover all of those scenarios.
  2. KIF - then for UI testing I use KIF because its test actor helps handle async and view loading delays. With KIF I can post a notification from my code and my test will wait to see the effects of my notification handler in the view.

Between those 2 systems I'm able to unit-test pretty much everything and then very easily write the UI tests to cover the final parts.

Also, quick note on your code: I wouldn't add your observers in viewWillAppear because it is called more than once. However, it may not be an issue since you probably won't get redundant calls to your handler because of notification coalescing.

like image 27
Mike Gottlieb Avatar answered Oct 03 '22 05:10

Mike Gottlieb