cat /dev/brain > /dev/random

30 December
2005

Unit Testing Objective-C with Python

Have you ever wanted to write unit tests for Objective-C, but found the Objective-C unit testing frameworks less than appetizing? Or have you ever thought that Objective-C didn't lend itself to the rapid development needed to efficiently write Unit Tests? If so, then this info is for you.

I don't want to start a language war here, but I will say this, personally, I love Cocoa, and find Objective-C a tolerable language to code in, but I really find it far too typing intensive to make it truly pleasurable enough to make me WANT to write unit tests in it. Many other sites have made comparisons of various languages, so I'll say no more about that right now. For now, consider the following procedure, if you want to try it yourself, for writing unit tests for Objective-C in python!

  1. Download and install PyObjC.
  2. Add a new Cocoa application target to your Obj-C project.
  3. Create a new Obj-C class called "PythonGlue" and make it look like this[1]:
     /*
     PythonGlue is a class implementing a singleton object that does
     nothing, but it has one side effect: it initializes Python (which
     should be linked into the bundle containing this class) and executes
     Contents/Resourcs/PythonGlue.py from the main bundle.
    
     No error checking is done, but Python errors will result in messages
     on standard error (or the console, for programs started from the   Finder).
     */
    
     #import Foundation/Foundation.h
     #import "PythonGlue.h"
     #import Python/Python.h
     #import stdio.h
    
     @implementation PythonGlue
    
     - init
     {
        static id _singleton;
        NSString *path;
        const char *c_path;
        FILE *fp;
    
        if (_singleton) return _singleton;
        _singleton = self;
        path = [[[NSBundle mainBundle] resourcePath] 
                  stringByAppendingPathComponent: @"PythonUnitTests.py"];
        c_path = [path cString];
        if ((fp=fopen(c_path, "r")) == NULL) {
            perror(c_path);
            return self;
        }
    
            // check that we are running on something that supports python (weakly linked)
            if(Py_Initialize)
            {
                    Py_Initialize();
                    PyRun_SimpleFile(fp, c_path);
            }
        return self;
     }
    
     @end
    
  4. Create a file called "TestMain.m" and put this in it:
     #import Cocoa/Cocoa.h
     #import "PythonGlue.h"
    
     int main(int argc, const char *argv[])
     {
            NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 
            PythonGlue *glue = [[PythonGlue alloc] init];
        return NSApplicationMain(argc, argv);
     }
    
  5. Create a new nib called "TestMainMenu" and put whatever you want in it. It's contents are unimportant. [2]
  6. Open the properties of your new target, go to the "Properties" tab and set your main nib file to "TestMainMenu". [2]
  7. If you run the target now you should just get your dummy nib file. Does it work? Good.
  8. Create another new target-this time a framework. Call it "TestingFramework". All the classes you want to test will need to be added to this framework. If you want to add them now, go ahead.

At this point, all the Obj-C bits are done, from here it's all python.

  1. Now what we will do is look for all files ending with ''*Test.py" and import them. Then run the pyunit test runner to execute the actual test. Create a python file called "PythonUnitTests.py" and make it look like this [1]:
     # Skeleton Python source for embedding Python into ObjC programs.
     # This source file expects to be run by the ObjC code in PythonGlue.m
     # and it expects to live in Contents/Resources of some .app bundle.
     # It will add the Resources folder and its PyObjC subfolder to
     # sys.path and import any modules found in Resources (which in
     # turn makes any PyObjC classes in these modules available to the
     # ObjC runtime system).
     import os
     import sys
     import unittest
    
     DEBUG = 0
    
     def main():
         # First find the Resource folder of the current application
         resource_folder, ourname = os.path.split(__file__)
         if DEBUG:
             print "PythonGlue: resource folder:", resource_folder
    
         # Add this folder and the PyObjC subfolder to sys.path
         sys.path.append(resource_folder)
         sys.path.append(os.path.join(resource_folder, "PyObjC"))
    
         # Now import all modules from the resource folder
         files = []
         for filename in os.listdir(resource_folder):
             if filename[-7:] == "Test.py" and filename != ourname:
                 module_name = filename[:-3]
                 files.append(module_name)
         if len(files) == 0:
             print "PythonGlue: Warning: no Python modules found"
    
         modules = map(__import__, files)
         testSuites = map(unittest.defaultTestLoader.loadTestsFromModule, modules)
         fullSuite = unittest.defaultTestLoader.suiteClass()
         for suite in testSuites:
              fullSuite.addTests(suite._tests)
         runner = unittest.TextTestRunner(verbosity=2)
         if(runner.run(fullSuite).wasSuccessful()):
              sys.exit(0)
         else:
              sys.exit(1)
    

main()

  1. We use PyUnit which is part of the standard Python installation to do our actual unit testing. PyUnit looks for any class derived from TestCase-it then executes any methods within it that start with "test" . Inside these python files, we need only refer to our classes using the standard PyObjC calling conventions to test our classes. So finally, create another file called "MyClassTest.py" the boilerplate for this is as follows:
     from unittest import *
    
     import objc
     objc.loadBundle("TestingFramework", globals(), 
        bundle_path='/TestingFramework.framework')
     del objc
    
     class MyClassTest(TestCase):
          def setUp(self):
              # do setup code here...
              self.myObj = MyClass.alloc().init()
    
          def tearDown(self):
              # do teardown code here.
              del self.myObj
    
         def testFoo(self):
              # your actual tests go in functions like this one.       
              self.assertEqual(self.myObj.myFunction(), True)
    

So, you can now write unittests for your Objective-C code using Python! Rock on!

[1] This code is derived from the PyObjC docs here

[2] I am willing to bet this is not required, but it was the way I felt safe doing this. Please tell me a better way if you know one.


Posted by jiva at 14:58 | Comments (0) | Trackbacks (0)
Comments
There are no comments.
Trackbacks
There are no trackbacks.