I finally made some time to update the Pip tool download link based on some folks kindly reaching out regarding a broken download link. A new link is listed below and I also took a little time to fix a few more bugs as well! :)
- PySide2 Support for Maya 2018+
- Fixed a bug with the tutorial throwing errors in niche cases where the tool was excited at specific times.
- Minor code cleanup and refactor.
Download Pip 2023 Link
Monday, February 20, 2023
Monday, October 25, 2021
Celebrating Vader Immortal
It's been a while since my last blog post, I do have a few topics noted that I want to do write ups on so hopefully this area will be a bit livelier soon!
Looking back and seeing that my last post was almost 5 years ago really helps you reflect on what has happened in five years. I'll keep it focused on Tech Art as that's the purpose of this blog :)
One of the reflections I had was to celebrate what a success Vader Immortal was as a VR series on the Oculus Rift and as a launching title for the Oculus Quest! It was a finalist for an Emmy nomination for "Outstanding Innovation in Interactive Media" in 2019. And it was awarded with the 2020 Game Developers Choice Award for Best VR/AR Game. It's amazing what was accomplished on such a young platform and I'm in awe of the talent and the team that contributed to that project.
Darth Vader was the first hero character I was assigned when I started at Lucasfilm/ILMxLab - it was quite crazy to think this was the first digital Vader contributed to the Asset Database since he's always been an actor on set. This Vader was used for several Epic presentations, some R&D projects, the VOID's Secret of the Empire experience and then finally in Vader Immortal Episodes 1-3. I remember going out and buying a Lego Vader keychain around that time to celebrate, and I look back fondly at those early days of getting assigned the task, seeing guests flip out when they see Vader in Secrets of the Empire or whenever someone realizes how tall he is when they see him in Vader Immortal.
Memory lane, sometimes its the simple things that you look back on and they make you feel like a kid again :)
Vader Immortal Ep 1 Trailer
Vader Immortal Ep 2 Trailer
Vader Immortal Ep 3 Trailer
Looking back and seeing that my last post was almost 5 years ago really helps you reflect on what has happened in five years. I'll keep it focused on Tech Art as that's the purpose of this blog :)
One of the reflections I had was to celebrate what a success Vader Immortal was as a VR series on the Oculus Rift and as a launching title for the Oculus Quest! It was a finalist for an Emmy nomination for "Outstanding Innovation in Interactive Media" in 2019. And it was awarded with the 2020 Game Developers Choice Award for Best VR/AR Game. It's amazing what was accomplished on such a young platform and I'm in awe of the talent and the team that contributed to that project.
Darth Vader was the first hero character I was assigned when I started at Lucasfilm/ILMxLab - it was quite crazy to think this was the first digital Vader contributed to the Asset Database since he's always been an actor on set. This Vader was used for several Epic presentations, some R&D projects, the VOID's Secret of the Empire experience and then finally in Vader Immortal Episodes 1-3. I remember going out and buying a Lego Vader keychain around that time to celebrate, and I look back fondly at those early days of getting assigned the task, seeing guests flip out when they see Vader in Secrets of the Empire or whenever someone realizes how tall he is when they see him in Vader Immortal.
Memory lane, sometimes its the simple things that you look back on and they make you feel like a kid again :)
Vader Immortal Ep 1 Trailer
Vader Immortal Ep 2 Trailer
Vader Immortal Ep 3 Trailer
Sunday, January 1, 2017
Career Paths
I have given a lot of thought about career paths (mostly while riding the bus around town, something I also never thought I would do when I was younger). In school, you are taught about the categorization of art disciplines within art industries and how that helps to put you "on rails to getting a job and setting your career". I knew I had a passion for rigging characters, that has taken me across job titles, across industries and across disciplines some times. I've had the opportunity to work on some film, some real time work and now it is a mixture of the two. This is definitely not something I could have imagined my career to be like when I was in college about 10 years ago - and I am glad things did not work as I had planned back then - it has been far better.
Saturday, August 27, 2016
Update to PiP (bug fixes)
pip_tool.zip
I've been trying to stay up to date on emails I've received for bugs that have popped up with PiP, I wanted to cover one of the bugs. The interactive tutorial included with PiP is displayed on the bottom of the PiP instance widget in a green bar with a QLabel widget. I noticed even after the completion of the tutorial that interacting with the resize button would sometimes cause the green bar to turn back on...
Here is the fix...
So it was just a simple if statement with comparing the use of the tutorial widget. My green tutorial bar is just a simple QLabel with a green style sheet background applied to it, when the tutorial is active it should automatically resize with the window. In an instance when it is NOT active - such as after the tutorial is completed - it should go away (width value of 0). So a simple check of a valid width will suffice in determining if we want to update the width along with the window resize changes.
I've been trying to stay up to date on emails I've received for bugs that have popped up with PiP, I wanted to cover one of the bugs. The interactive tutorial included with PiP is displayed on the bottom of the PiP instance widget in a green bar with a QLabel widget. I noticed even after the completion of the tutorial that interacting with the resize button would sometimes cause the green bar to turn back on...
PiP tutorial QLabel widget with css style |
1 2 3 | # update tutorial label to resize to window self.main_class.label.setFixedSize( QtCore.QSize( new_width, self.main_class.label.height() ) ) self.main_class.label.move( 0, self.parent().height() - self.main_class.label.height() ) |
Here is the fix...
1 2 3 4 5 | # update tutorial label to resize to window if self.main_class.label.width() > 1: self.main_class.label.setFixedSize( QtCore.QSize( new_width, self.main_class.label.height() ) ) self.main_class.label.move( 0, self.parent().height() - self.main_class.label.height() ) |
So it was just a simple if statement with comparing the use of the tutorial widget. My green tutorial bar is just a simple QLabel with a green style sheet background applied to it, when the tutorial is active it should automatically resize with the window. In an instance when it is NOT active - such as after the tutorial is completed - it should go away (width value of 0). So a simple check of a valid width will suffice in determining if we want to update the width along with the window resize changes.
Thursday, July 7, 2016
Scripting A Simple Pose Reader
I recently learned this setup and just wanted to share a scripted version I had been working on. For anyone who has never used a pose reader setup, it's a fantastic way to make your actions that are driven by specific poses to be more stable. When I first learned rigging in school, I was taught to use set driven keyframes to drive an object based on a certain pose, so I included that setup in this blog as a comparison to the simple pose reader setup. As a side note, the Pirate rig that is in my latest demo reel actually utilized this "simple pose reader" quite a bit - as I was learning the technique I started scripting it. I used it to drive things like special deformation joints, corrective blendshapes, auto hips and shoulders, as well as accessories on the pirate's belt would move as the user interacts with the leg controls. It was quite fun to work with such a simple and effective setup.
Anyway, here is a quick gif video of an example comparing two setups, the green is an action driven by a pose through an SDK - and the red is an action driven by a pose through the PSR.
The SDK setup is a simple, the joint's rotateZ attribute from 0-90 will drive the translateY attribute of the sphere from 0-1. Very limited without adding more animations.
Here is the simple graph setup for the PSR to drive the movement of the sphere. To summarize, you are utilizing the PSR's controlled rotation values and remapping those values to different values that are sent over to the sphere (in this instance a single rotation of the joint drives the translate y of the sphere).
And here is the script I was working with...
Anyway, here is a quick gif video of an example comparing two setups, the green is an action driven by a pose through an SDK - and the red is an action driven by a pose through the PSR.
The SDK setup is a simple, the joint's rotateZ attribute from 0-90 will drive the translateY attribute of the sphere from 0-1. Very limited without adding more animations.
Here is the simple graph setup for the PSR to drive the movement of the sphere. To summarize, you are utilizing the PSR's controlled rotation values and remapping those values to different values that are sent over to the sphere (in this instance a single rotation of the joint drives the translate y of the sphere).
And here is the script I was working with...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | # Maya imports import pymel.core as pm import maya.OpenMaya as om def simple_pose_reader( root_joint ): """ Function to create a simple pose reader with "bend", "twist", "side" attributes on the selected joint to use to drive other systems Args: root_joint (pynode) : a pynode object representing the main joint used for the pose reading Returns: None """ def first_or_default( sequence, default=None ): """ Function to return the first item in a list or a default value Args: sequence (list) : a list of items to parse default (object) : a default value to return back if nothing was parsed from sequence Returns: The first object within the sequence or a default value """ for item in sequence: return item return default def get_bone_draw_axis( joint, default=om.MVector(0,1,0) ): """ Function to return the first item in a list or a default value Args: joint (pynode) : the pynode that represents the joint to determine the draw_axis of default (object) : a default value to return back if the draw_axis was not determined Returns: A MVector type that represents the normalized direction of the joint draw axis """ try: child = first_or_default( joint.getChildren( type='joint' ) ) except: raise ValueError( joint + " does not have any children" ) # Get the local position of the child joint ( localspace is offset from the parent space ) pos = [ value for value in child.getTranslation( localSpace=True ) ] # Check which axis is greater than the others, this will determine the draw_axis vector # X Axis if abs( pos[0] ) > abs( pos[1] ) and abs( pos[0] ) > abs( pos[2] ): if pos[0] > 0.0: return om.MVector( 1, 0, 0 ) return om.MVector( -1, 0, 0 ) # Y Axis elif abs( pos[1] ) > abs( pos[0] ) and abs( pos[1] ) > abs( pos[2] ): if pos[1] > 0.0: return om.MVector( 0, 1, 0 ) return om.MVector( 0, -1, 0 ) # Z Axis elif abs( pos[2] ) > abs( pos[0] ) and abs( pos[2] ) > abs( pos[1] ): if pos[2] > 0.0: return om.MVector( 0, 0, 1 ) return om.MVector( 0, 0, -1 ) return default def snap_to_transform( snap_transform, snap_to_transform ): """ Function to snap a transform to another transform based on position and orientation Args: snap_transform (pynode) : the object to snap snap_to_transform (pynode) : the object to snap to Returns: None """ snap_transform.setTranslation( snap_to_transform.getTranslation( worldSpace=True ), worldSpace=True ) snap_transform.setRotation( snap_to_transform.getRotation( worldSpace=True ), worldSpace=True ) # ensure pynode root_joint = pm.PyNode( root_joint ) setup_dict = { 'root_joint' : root_joint, 'child_joint' : first_or_default( root_joint.getChildren( type='joint' ) ), 'parent' : root_joint.getParent() } # create pose reader attributes on root_joint for attr in [ 'Bend', 'Twist', 'Side' ]: setup_dict['root_joint'].addAttr( 'psr_' + attr.lower(), attributeType='float', niceName='PSR ' + attr, keyable=True ) attr_name_list = [ setup_dict['root_joint'].name(), '.psr_', attr.lower() ] setup_dict[ 'attr_psr_' + attr.lower() ] = pm.PyNode( ''.join( attr_name_list ) ) # create organizing groups setup_dict[ 'psr_main_grp' ] = pm.group( name=setup_dict['root_joint'].name() + '_psrMain_GRP', empty=True ) setup_dict[ 'psr_target_grp' ] = pm.group( name=setup_dict['root_joint'].name() + '_psrTarget_GRP', empty=True ) setup_dict[ 'psr_twist_grp' ] = pm.group( name=setup_dict['root_joint'].name() + '_psrTwist_GRP', empty=True ) # create locators for loc in [ 'psrMain', 'psrMainTarget', 'psrMainUp', 'psrTwist', 'psrTwistTarget', 'psrTwistUp' ]: loc_name_list = [ setup_dict['root_joint'].name(), '_', loc, '_LOC' ] setup_dict[ loc ] = pm.spaceLocator( name= ''.join( loc_name_list ) ) setup_dict[ loc ].setParent( setup_dict['psr_main_grp'] ) # target locators parent under the target group, which is driven by the root_joint # the main grp is parented under the root_joint parent to maintain aiming without # taking in extra transforms from the root_joint or it's children [ setup_dict[ item ].setParent( setup_dict[ 'psr_target_grp' ] ) for item in [ 'psrMainTarget', 'psrTwistTarget' ] ] [ setup_dict[ item ].setParent( setup_dict[ 'psr_main_grp' ] ) for item in [ 'psr_target_grp', 'psr_twist_grp' ] ] [ setup_dict[ item ].setParent( setup_dict[ 'psr_twist_grp' ] ) for item in [ 'psrTwist', 'psrTwistUp' ] ] if setup_dict[ 'parent' ]: setup_dict[ 'psr_main_grp' ].setParent( setup_dict[ 'parent' ] ) # align main group to the selected root joint snap_to_transform( setup_dict[ 'psr_main_grp' ], setup_dict[ 'root_joint' ] ) draw_axis = get_bone_draw_axis( setup_dict['root_joint'] ) child_trans = setup_dict[ 'child_joint' ].getAttr( 't' ) child_offset = om.MVector( child_trans[0], child_trans[1], child_trans[2] ) # X draw axis if draw_axis == om.MVector( 1, 0, 0 ) or draw_axis == om.MVector( -1, 0, 0 ): main_up_offset = om.MVector( child_offset.x, child_offset.x, 0 ) main_up_vector = om.MVector( 0, -1, 0 ) twist_target_offset = om.MVector( 0, child_offset.x, 0 ) setup_dict[ 'twist_driver_rot' ] = [ '.rotateY', '.rotateX', '.rotateZ' ] # Y draw axis elif draw_axis == om.MVector( 0, 1, 0 ) or draw_axis == om.MVector( 0, -1, 0 ): main_up_offset = om.MVector( 0, child_offset.y, child_offset.y ) main_up_vector = om.MVector( 0, 0, -1 ) twist_target_offset = om.MVector( 0, child_offset.y, 0 ) setup_dict[ 'twist_driver_rot' ] = [ '.rotateZ', '.rotateY', '.rotateX' ] # Z draw axis else: main_up_offset = om.MVector( child_offset.z, 0, child_offset.z ) main_up_vector = om.MVector( -1, 0, 0 ) twist_target_offset = om.MVector( 0, 0, child_offset.z ) setup_dict[ 'twist_driver_rot' ] = [ '.rotateX', '.rotateY', '.rotateY' ] setup_dict[ 'psrMainTarget' ].setTranslation( child_offset * 0.5, localSpace=True, relative=True ) for loc in ['psrMain', 'psrTwistUp']: setup_dict[ loc ].setTranslation( child_offset * -1.0, localSpace=True, relative=True ) setup_dict[ 'psrTwistTarget' ].setTranslation( twist_target_offset * -1.0, localSpace=True, relative=True ) setup_dict[ 'psrMainUp' ].setTranslation( main_up_offset * -1.0, localSpace=True, relative=True ) setup_dict[ 'psrMainAC' ] = pm.aimConstraint( setup_dict[ 'psrMainTarget' ], setup_dict[ 'psrMain' ], maintainOffset=True, aimVector=[ draw_axis.x, draw_axis.y, draw_axis.z ], upVector=[ main_up_vector.x, main_up_vector.y, main_up_vector.z ], worldUpType='objectrotation', worldUpObject=setup_dict[ 'psrMainUp' ].name(), weight=1.0 ) setup_dict[ 'psrTwistAC' ] = pm.aimConstraint( setup_dict[ 'psrTwistTarget' ], setup_dict[ 'psrTwist' ], maintainOffset=True, aimVector=[ main_up_vector.x, main_up_vector.y, main_up_vector.z ], upVector=[ draw_axis.x * -1, draw_axis.y * -1, draw_axis.z * -1 ], worldUpType='objectrotation', worldUpObject=setup_dict[ 'psrTwistUp' ].name(), weight=1.0 ) setup_dict[ 'psr_target_grp' ].setParent( setup_dict[ 'root_joint' ] ) # the bend and side rotations of main psr locator drives the twist psr grp # this allows the child twist locator (under the twist psr grp) to maintain # an accurate twist rotation only pm.connectAttr( setup_dict['psrMain'] + setup_dict['twist_driver_rot'][0], setup_dict[ 'psr_twist_grp'] + setup_dict['twist_driver_rot'][0], force=True ) pm.connectAttr( setup_dict['psrMain'] + setup_dict['twist_driver_rot'][2], setup_dict[ 'psr_twist_grp'] + setup_dict['twist_driver_rot'][2], force=True ) # connect final calculations to the custom attributes on the root_joint # these can be used for various setups that are driven from the pose reader pm.connectAttr( setup_dict['psrMain'] + setup_dict['twist_driver_rot'][0], setup_dict[ 'attr_psr_bend' ], force=True ) pm.connectAttr( setup_dict['psrMain'] + setup_dict['twist_driver_rot'][2], setup_dict[ 'attr_psr_side' ], force=True ) pm.connectAttr( setup_dict['psrTwist'] + setup_dict['twist_driver_rot'][1], setup_dict[ 'attr_psr_twist' ], force=True ) selection = pm.ls( selection=True ) if selection: simple_pose_reader( selection[0] ) |
Sunday, July 3, 2016
Maya Picture in Picture Tool (PiP) Development Part 2
I haven't had too much time to devote to getting PiP fully complete in a while - I am to a point now where I would like to put up a beta download and see what kind of feedback I get for improvements.
pip_tool.zip
Just unzip the pip_tool folder to your Maya python path (example, your documents/maya/scripts/ folder). The code to launch the tool, from a Python tab in the Maya script Editor...
This can be dragged to a shelf for later use.
I recently posted about my exploration with unit tests, I wanted to also post some of my early work with unit testing for PiP. I feel like most folks who are early on in their learning path with unit tests would benefit from seeing examples of what kinds of things to test for. This isn't my most current test library for PiP but it should get the point across...
From these examples, you can see I started with testing the results of early development stages such as moving files from my development path to the Maya script path, or just a simple import check, UI loading checks, etc. Kind of a neat thing to note here is my unit tests are being ran from the windows command line to launch a standalone Maya session (no UI). The problem with this is PiP is a UI based tool, so as I kept developing more tests I realized I could run some tests in command line but to run some specific tests I would need a normal Maya session.
The current beta release seems to have a few graphical glitches on certain computer setups - that and a few other "nice to haves" are the known issues that I am wanting to polish before I consider it complete. Anyway, that's all for now!
pip_tool.zip
Just unzip the pip_tool folder to your Maya python path (example, your documents/maya/scripts/ folder). The code to launch the tool, from a Python tab in the Maya script Editor...
1 2 | import pip_tool.pip as PiP PiP.jbPiP_UI() |
This can be dragged to a shelf for later use.
I recently posted about my exploration with unit tests, I wanted to also post some of my early work with unit testing for PiP. I feel like most folks who are early on in their learning path with unit tests would benefit from seeing examples of what kinds of things to test for. This isn't my most current test library for PiP but it should get the point across...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | """ Jason Breneman unittest_pip.py This file contains the unit test library for the Picture in Picture tool. Some unit tests do not pass specifically due to launching Maya through a standalone session. """ # maya libraries import maya.standalone import maya.cmds as cmds try: maya.standalone.initialize() except: pass # python libraries import unittest import os import uuid import shutil import logging logging.basicConfig( level=logging.INFO ) logger = logging.getLogger( __name__ ) logger.info( "PiP Unit Test starting...\n" ) MAYA_VERSION = cmds.about( version=True ) MAYA_APP_DIR = os.environ["MAYA_APP_DIR"] ROOT_DEV_DIR = sys.path[0].replace( "\\", "/" ).replace( "/pip_tool/tests", "" ) ROOT_TOOL_PATH = MAYA_APP_DIR + "/scripts" logger.info( "'Maya Version' : " + MAYA_VERSION + "\n" ) logger.info( "'MAYA_APP_DIR' : " + MAYA_APP_DIR + "\n" ) class TestLibrary( unittest.TestCase ): """ Test Library unit test class for Picture in Picture tool """ def setUp( self ): """ Method to provide any standard setup instructions for a test case function Args: self (object) : reference to the TestLibrary class instance Returns: None """ pass def tearDown( self ): """ Method to provide any standard tear down instructions for a test case function Args: self (object) : reference to the TestLibrary class instance Returns: None """ pass def test_files_installed_check( self ): """ Test method for copying files from the development environment to the MAYA_APP_DIR path Args: self (object) : reference to the TestLibrary class instance Returns: None """ logger.info( "*** TestCase *** Remove existing install, and apply a fresh install\n" ) if os.path.exists( ROOT_TOOL_PATH + "/pip_tool" ): shutil.rmtree( ROOT_TOOL_PATH + "/pip_tool" ) shutil.copytree( ROOT_DEV_DIR + "/pip_tool", ROOT_TOOL_PATH + "/pip_tool" ) def test_import_check( self ): """ Test method to check for a successful module import Args: self (object) : reference to the TestLibrary class instance Returns: None """ logger.info( "*** TestCase *** Import pip_tool module\n" ) import_success = False error_message = "TestCase Failure, PiP module did not import correctly.\n" try: import pip_tool.pip as PiP import_success = True except: pass self.assertTrue( import_success, error_message ) def test_pip_instance( self ): """ Test method to check a successful instantiation of PiP Args: self (object) : reference to the TestLibrary class instance Returns: None """ logger.info( "*** TestCase *** Load a PiP instance\n" ) import pip_tool.pip as PiP reload( PiP ) loadout = PiP.jbPiP_UI() loadout_exists = cmds.modelEditor( loadout.name_instance + "__ME", query=True, exists=True ) error_message = "TestCase Failure, PiP loadout does not exist. Maya UI required for successful TestCase\n" self.assertTrue( loadout_exists, error_message ) def test_pip_callback_newscene( self ): """ Test method to check if the new scene callback gets properly deleted when a new scene event occurs Args: self (object) : reference to the TestLibrary class instance Returns: None """ logger.info( "*** TestCase *** Delete PiP on New Scene Callback\n" ) import pip_tool.pip as PiP reload( PiP ) loadout = PiP.jbPiP_UI() cmds.file( new=True, force=True ) self.assertEquals( loadout.newscene_callbackid, None ) # if starting from a command prompt, load unit test class instance if __name__ == "__main__": unittest.main() |
From these examples, you can see I started with testing the results of early development stages such as moving files from my development path to the Maya script path, or just a simple import check, UI loading checks, etc. Kind of a neat thing to note here is my unit tests are being ran from the windows command line to launch a standalone Maya session (no UI). The problem with this is PiP is a UI based tool, so as I kept developing more tests I realized I could run some tests in command line but to run some specific tests I would need a normal Maya session.
The current beta release seems to have a few graphical glitches on certain computer setups - that and a few other "nice to haves" are the known issues that I am wanting to polish before I consider it complete. Anyway, that's all for now!
Wednesday, June 29, 2016
Discovering the Benefits of Unit Testing and TDD
In the past few years I kept hearing the term unit testing pop up here and there. I seemed to always brush it aside, I figured I would eventually find time to learn about it what it is and why I kept hearing so much buzz about it. Well I finally found the time when I was developing my picture in picture tool. And I must say, I am now convinced of how awesome it is.
I won't go super in depth, there are plenty of blogs, documentation and the like that go over the details of unit testing (python docs link). I will say that when utilizing unit testing, I feel much more confident in the stability of my code and my ability to track down future bugs.
Unit testing in general can be described as implementing small testing methods that validate small bits of actual code. It may seem silly to write "test code" for your code. Typically what I refer to as "linear coding", you end up testing your code yourself while you write it. So why spend all that extra time on code that isn't used within the tool? Sanity, is my answer. I've built small tools and large tools, there are always easy bugs and hard bugs to find within the code. Sometimes finding a bug is simple, but it's always at least a matter of minutes to fix something, and it would be nice if I could apply the fix, have all my tests pass with said fix and be confident that the code is now fixed as well as stable. After spending the time reading up on unit testing, I found that I regretted not researching it sooner.
Along my path of learning about unit testing, I have also created an initiative at work to try and train the rest of the TAs to understand and implement unit testing within their normal tool development process. While researching unit testing, it eventually lead me to learning about Test Driven Development (TDD).
The concept of TDD, being that you start with writing your unit tests that are setup to actually fail, that failure then prompts you to create the code to make the test succeed, and as you add new code you refactor the existing code. I actually really enjoy this methodology, as it is a very helpful guide to creating very stable code. I have noted a few helpful things I have been practicing this on my own..
- Write a lot of useful and relevant unit tests, I found that breaking my code down into testable chunks helped me to consider all the areas that could have led to possible future bugs. This actually helped me in understanding why some pieces of code could be more bug prone. The more tests, the more detailed report of the stability of your code. Along with this, I would be sure those test functions are very narrow and focused on a specific testable variable.
- Find a new bug? Write a new test if you can, and add it to your test suite. This will help continue the population of updated unit tests and will help keep your code stable as bugs are fixed.
- Readability and good commenting count, especially in your test suite methods! The point of these unit test suites is to improve the quality of your code and to assist in future bug fixes. It's always good to consider the future bug fixer may not be you. So commenting and properly naming the unit test function is essential. The entire purpose of running the test suite is to quickly find bugs, if it's difficult to read or understand what the test suite is doing it may slow down the process of fixing said bug(s). I tend to like the function naming that most people suggest: test_import_module_validation() instead of something like test_import(). In this instance, try to avoid short handing the name of a function.
- Take advantage of the error message argument within the assert functions. The purpose of these unit tests is to provide success/failure checks along with information, the error message is a great opportunity to provide more information to the test runner on how the test failed. It's great to use unit tests within TDD to make guide you in creating solid code, but always remember that the person doing the tests in the future may not be the original author, so the more information the better.
- Not necessarily related to learning/writing unit tests, but if you are learning with the goal of implementing this among a team it's a good idea to document everything. What it is, how to use it, best practices, pros, cons - the works. Provide a path for the team to learn the information (direction), encourage them to refer back to the information (documentation) when needed, and also give them a time to learn it (training).
Subscribe to:
Posts (Atom)