I have a confession to make: I've always looked at test driven development with envy and admiration, but I had never really tried it due to the strong dependencies of the code I usually write... but it's even worse, I had never put a fair amount of effort on testing due to the same reason (I know, shame on me).
Until one day I finally swallowed my excuses and decided to go TDD on a
proper project (
mauto, more on this in a future post)... and you know what?
it worked great!
I'm taking my first steps on this and I have no authority to 'teach' TDD, but I'm sure there are many TDs/technical-artists with the same preconceptions on testing and hopefully some of my humble findings could be useful for them.
Why testing? I know it's a good practice but...
That was exactly my thought for a long time: "this code is not a core library so it doesn't matter", "I have so many dependencies, I can't test this", "it would be nice but maybe later", "write tests sucks" and so on.
The thruth is: writing tests leads you to write better code and enables you to refactor your stuff without breaking everything as you do it. Think of it as a tool to back you up as you progressively refine the project.
Whithout tests you can't really work this way, your first take has to be the good one (which is never the case) and any later modification comes with a huge penalty, breaking existing code is inevitable.
What about dependencies?
Most tools I write are meant to be executed within a DCC (maya, softimage, whatever) and there's a strong dependency on their APIs... So, how to test this stuff?
Encapsulate your dependencies! write testable code is up to you.
The first change I noticed on myself was the need to think about dependencies in order to test my code, I started to encapsulate stuff instead of call expensive resources all over the place (i.e. writing all calls to the filesystem in a particular module, DCC calls on other and so on).
I'm not saying you have to write a complex software-agnostic wrapper or anything like that, just do your best to keep things thight in order to know where your dependencies are, otherwise you won't be able to replace them when testing your code.
Forcing yourself to be aware of dependencies seems to be a win/win situation as it enforces a clear separation of concerns on your code (even if you are not going to write any test).
Sounds good, where to start?
We need to define a structure for our project! I highly recommend stick to the standard structure for python projects/packages, here's a quickstart guide.
You can also create your base structure using something like coockiecutter or similar tools, most IDEs set a base project for you.
In order to rely on your tests to develop is extremely important have a mechanism to systematicaly run the test suite, it sounds obvious but it's not.
Python include several ways to write/run tests, for really simple use cases you can go with doctest, but most of the time you will be better of writing your test in a separate module using somthing like unittest.
Other important aspect of testing has to do with replacing the expensive resources during testing, this way you can run them faster and without affecting production assets in case something goes wrong (databases and what not).
For this situations there's a python library called mock. I won't cover how to use it, but it provides a nice placeholder that you can use to monkeypatch your dependencies during your tests. For further information refer to its documentation.
Everything is set, now what?
Short answer: start with the readme file!
This is not TDD per-se, but write down a high level snippet about how to use your code is really useful and gives you a nice starting point.
Let's say we want to develop a pose library (this comes from the top of my head, please do not take this example too seriously):
import pose_lib # init rig from the scene ROOT_NODE = "C_John_root_C_GRP" # node name rig = pose_lib.Rig(ROOT_NODE) # list john compatible poses pose_lib.list_poses('John') # apply a pose to John's rig pose = pose_lib.get_pose('John', 'stand') rig.set_pose(pose) # add John's current pose to the library pose = rig.current_pose() pose_lib.add_pose('John', 'my_pose', pose) # and so on...
Boom! We have just decided to interface with maya nodes through a class called Rig and access the library through a couple of functions (there's probably another class managing json-files/sqllite/any-kind-of-storage under the hood).
Just think how you would like interact with your finished project and write it down (wait a minute, we are also documenting!).
Let's write some code!
First, let's assume this structure for our project:
The first test is the hardest! that's why the readme is so important.
1- Reading the readme snippet I would go writting a test as follow:
# file: pose_lib/pose_lib/tests/test_library.py from nose import test_with import pose_lib def setup(): pose_lib.add_set('test') def tear_down(): pose_lib.remove_set('test') @test_with(setup, tear_down) def test_list_poses() assert isinstance(pose_lib.list_poses('test'), (list, tuple))
teardownfunctions are executed before and after the test function (the decorated function), other testing frameworks (this one is
nosespecific) should provide a similar way to do this.
2- ... which lead us to write this:
# file: pose_lib/pose_lib/__init__.py def add_set(set_name) pass def remove_set(set_name) pass def list_poses(set_name): pass
3- ... and run the tests (from the root directory in a terminal):
python setup.py nosetests
Most IDEs and code editors have a built in function to run the test suite for you, if you're using an editor like sublime text you can take advantage of the build system to run the tests.
4- ... and refactor the code until all tests pass...
5- ... and write new tests...
6- ... and repeat until the project is completed.
Do you see where we're going with this?
Is that simple! of course there will be some work to do in modules with dependencies (take a look at mock's docs; there are ways to patch objects, simulate return values and so on). With not-so-much effort you should be able to run your tests outside maya/softimage/whatever as any other python developer (that means you can go faster and lighter on early stages of the project and hopefully write better code).
There's much more to say about testing and TDD, things like coverage and what not... but I think this is enough for a first contact.
Is it doable on the project you are working on? Do you have any experience working like this? Anything to share?
Would love to hear what you think about this, please leave me your comments below.