Friday, August 5, 2011

iOS Continuous Integration - Part II


In the previous post, we prepared out local GIT repositories for both the SampleLib and the SampleApp.

This post will describe how to prepare the library for unit testing, code coverage, and automated builds with Jenkins.
It will also show how to consume this library on your SampleApp.

Feel free to get a copy of the project from my github account, although if you follow this post you'll ended up with exactly what I have there, plus you'll be learning how to do it :-)

SampleLib

This assumes you already cloned the SampleLib repository under ~/Work/SampleLib (or whatever folder of your preference).
  • Launch Xcode4 and choose to Create a new Xcode project
  • Choose iOS Templates -> Framework & Library -> Cocoa Touch Static Library
  • Product Name = SampleLib and check the box to Include Unit Tests
  • Choose to save it under ~/Work/SampleLib - where you cloned SampleLib repository
    • Note: after that, there will be a 'SampleLib' folder under 'SampleLib' - root of the repository.
    • Suggestion is to close the Xcode project and move the contents one folder up to avoid confusion.
    • See screenshots for before and after moving the folders
    • Once you're done, re-open the project in Xcode.

Unit Tests

  • As common TDD practice, we should always start writing tests, run so it fails, write/test/rewrite the code until needed.
  • After a few iterations, these are the files I have for Unit Tests & NSDate+Humanize
  • Note: in order for you to run the tests, make sure you have the following flag set for the SampleLibTests Target
    • Other Linked Flags = -all_load -framework SenTestingKit

Internationalization

When looking at NSDate+Humanize.m you've noticed calls to NSLocalizedString, but we still don't have the properties file with those Strings. The reason for that is that we also want to automate the generation of Strings in order to avoid forgetting something.
  • Make sure you're viewing the "Project Navigator" (⌘1)
  • Click on the project SampleLib -> Targets -> SampleLib
  • Click on Build Phases
  • Add a new Run Script phase
  • Move it after "Target Dependencies" and before "Compile Sources"
  • Add the following code:
lpath=${SOURCE_ROOT}/SampleLib/Resources/en.lproj
mkdir -p ${lpath}
rm -f ${lpath}/*
find ${SOURCE_ROOT}/SampleLib -name "*.m" -print0 | xargs -0 genstrings -q -s NSLocalizedString -o ${lpath}
shopt -s nullglob
for strings in ${lpath}/*
 do
   echo "*** ${strings} ***"
   iconv -f UTF-16 -t UTF-8 ${strings} > ${strings}.utf8
   cat ${strings}.utf8 | grep -v "No comment provided by engineer." > ${strings}
   rm ${strings}.utf8
   cat ${strings}
done
  • Build again, and verify the script got called and the strings are generated, by looking at the Log Navigator (⌘7)
  • You also must add the generated en.lproj to SampleLib project
  • Go back to "Project Navigator" (⌘1)
  • Make sure you right click under "SampleLib", choose Add File to "SampleLib"..., and add the Resources folder

Preparing for Code Coverage

Now that we have Unit Testing and our library ready, we should start looking at the code coverage reports and make sure our tests are exercising as much as possible of our code.
  • Make sure you're in "Project Navigator" (⌘1) and click on the SampleLib project
  • create a *Coverage* configuration, by duplicating the "Debug" Configuration
  • In order to validate the code coverage will be generated by Jenkins, let's make sure we can generate it from Xcode as well.
  • To do that, select Product -> Edit Scheme and change Test to use Coverage configuration and click Ok to close the dialog.
  • Select the testing target SampleLibTests -> Build Phases -> Run Script and change it to:
# Run the unit tests in this test bundle.
"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests"

# Run gcov on the framework getting tested
if [ "${CONFIGURATION}" = 'Coverage' ]; then
    # MUST manually set this, as the project runs from SampleLib
    FRAMEWORK_NAME=SampleLib
    FRAMEWORK_OBJ_DIR=${CONFIGURATION_TEMP_DIR}/${FRAMEWORK_NAME}.build/Objects-normal/${NATIVE_ARCH}

    mkdir -p coverage
    pushd coverage
    find ${OBJROOT} -name *.gcda -exec gcov -o ${FRAMEWORK_OBJ_DIR} {} \;
    popd
fi 
  • Now, make sure you configure all the Build Settings as shown below
  • Pay special attention to differences for the Coverage settings
  • I find it makes it easier to select the SampleLib Project and the 2 targets: SampleLib & SampleLibTests, so that Xcode display a matrix with all the settings (see screenshot below)
- Compiler for C/C++/Objective-C -- set in Project level
    - Coverage      = GCC 4.2
    - Debug/Release = LLVM GCC 4.2

- Other Linker Flags -- set in Target Level
    - SampleLib + Coverage      = -lgcov
    - SampleLib + Debug/Release = -ObjC
    - SampleLibTests + All      = -all_load -framework SenTestingKit

- Library Search Paths -- set in Project level
    - Coverage      = /Developer/usr/lib/gcc/i686-apple-darwin11/4.2.1
    - Debug/Release = empty

- Test After Build -- set in Project level
    - Coverage   = Yes
    - all others = No 

- Generate Test Coverage Files -- set in Target Level
    - SampleLib + Coverage = Yes
    - all others           = No 

- Instrument Program Flow -- set in Target Level
    - SampleLib + Coverage = Yes
    - all others           = No 

- Precompile Prefix Header -- set in Target Level
    - SampleLib + Coverage = No
    - all others           = Yes

- User-Defined - "Add User-Defined Setting" -> GCOV_PREFIX
    - set it in all levels = coverage
  • You should now be able to choose Product -> Clean (⇧⌘K), Product -> Test (⌘U) and see the code coverage stats being displayed on the console output
  • If you don't, go through the settings list above and make sure you have all the settings correctly defined in your project
  • If still having problems, you can take a look at Complication no.5
  • After verifying all works fine, you might want to revert the Test scheme to run out of Debug configuration. Just go to Product -> Edit Scheme and change Test to use Debug configuration and click Ok to close the dialog.
    • Reason for that is that you'll want to see the cool report from Jenkins, and not the text-based output from Xcode.

Let's automate this -- Jenkins Configuration

If you have not yet committed the code to git up to this point, dude you're doing it wrong.... :)
Make sure all is added, committed and pushed to your master repository -- remember Part I with all that cool GIT setup?

If you already have a Jenkins machine running on your network, you might be able to re-use it. The only requirement is that it need to run on a MacOS based machine, so we can call xcodebuild. If you're feeling adventurous, you might also want to take a look at this post from Shine Technologies on Continuous Deployment of iOS Apps with Jenkins and TestFlight. It describes how to configure a Jenkins node on a MacOS machine, when the master Jenkins server runs on some other OS.

To simplify things, I have Jenkins running on the same MacOS machine. Also, differently from Shine Technologies' approach, I'll not be using Ray Yamamoto's Hudson plugin. Maybe in a future post, I'll give it a try and also integrate with TestFlight to have the complete solution.

Now, let's configure Jenkins:
  • As I said, I have Jenkins running on the same MacOS development machine. Not very recommended, but I have only 1 Mac now.
  • Get the following external plugins/packages for Jenkins
    • ocunit2junit.rb - from Jayway's blog Continuos Integration for XCode projects
    • Git and/or GitHub plugins - from Jenkin Dashboard -> Manage -> Manage Plugins -> Available
    • Jenkins Cobertura plugin - also from Dashboard -> Manage -> Manage Plugins -> Available
  • Once all plugins are installed, we're ready to create a New Job
    • Job Name = SampleLib
    • Build a free-style software project
    • Source Code Management = Git
    • Repository = /Users/YOURUSERNAME/Work/GIT-REPO/public/SampleLib.git or your GitHub repository URL
    • Build Triggers = Poll SCM (or any other) = * * * * * (to poll every minute) -- feel free to adjust to your likings
    • Build - add an Execute Shell step:
      xcodebuild -target "SampleLibTests" -configuration "Coverage" -sdk iphonesimulator4.3 | /usr/local/bin/ocunit2junit.rb
    • Build - add another Execute Shell step:
      gcovr -r SampleLib -e /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator4.3.sdk -x -o coverage/coverage.xml
    • Post-build Actions
      • check Publish Coberura Coverage Report, Cobertura xml report pattern = **/coverage.xml
      • check Publish JUnit test result report, Test report XMLs = test-reports/*.xml
  • You might want to configure other settings for your Jenkins jobs, following whatever standards on your company. The above are the ones needed for the code coverage automation.
  • Save the job and Build Now
  • If all goes correctly, you should see the code coverage report. To see the Unit test report, just run a 2nd build.
  • Below are some screenshots of the Jenkins configuration, and generated reports

Building the SampleApp

On the next post, we'll show how to use the lib on a SampleApp, and have it all building and testing with Jenkins.

1 comment:

  1. Great blog Rodrigo! I've been fighting with Automation, specifically iOS UI Automation using instruments. I can run my UI Automation tests from command line and I'm stuck at the point of trying to publish my UI Automation Results (specifically trying to convert the output plist generated by instruments) into Jenkins format (XML format Jenkins can understand)? Have you run into this. I've tried several scripts from github trying to convert the instruments plist for UI Automation to Jenkins format but not with much success. I want to show the details of UI Automation test results to Jenkins along with the pass/fail status(which I can convert ok). Please help!

    ReplyDelete