David Weller suggested that since I'm into DirectX and Test-Driven Development (TDD), I should post about how (in my experience) the two mix. I've gotten far enough now on my game to have run into a few issues, so it seemed like a good time to share.
The fundamental problem with TDD is that certain things are hard to test. Tests are easy to write when you're working with code that's designed to be called by you in some particular order - some sort of library. However, tests are much harder to write for certain mixtures of code that's written by you and called by you, code not written by you but is called by you, code not written by you that calls you, and so on.
The classic example of this is a Windows Form. A typical scenario here is that someone clicks the Open File menu item, which your code reacts to by popping up a dialog box asking for a file name, and then goes off and opens that file. It's a (fairly) complex interaction between code that you wrote and code that you didn't, calling back and forth between each other. Plus, we throw in interaction with the user. Similarly, in a game, we have users providing input via a keyboard or a joystick, to which our code reacts by rendering pixels onto a screen.
Obviously, to get automatability and repeatability, you don't want to have to require users to give input during a test. Particularly in a game, where a difference in user input as small as a microsecond could change the code path through the game. So a common approach is to separate out the pieces that you control from the pieces you don't, but abstracting them through an interface. When testing the application, you hand it an implementation of the interface that pretends to pop up a dialog box (or whatever), but actually always returns the same value. When running the application “for real”, you hand it an implementation of the interface that actually pops the dialog box (or whatever). These fake test-time objects are often called “mock objects”, and you can read more about them here.
The problem with using mock objects is that - although you try to get close to the same behavior - you (obviously) never quite make it. You're testing most of the code you'll be running in production with, but not all of it. I wanted to avoid this problem with my game, so I figured, “Well, I'll just run the tests against the real Direct3D objects.”
At first, everything was great. I was able to write tests that created windows, drove input (from a script, not from the keyboard) and asserted correct behavior. It meant that the unit tests would pop up windows while they were running, but it was sort of funny to watch the game play at like ten times normal speed. But then I tried to integrate the tests into the build.
I've set up Draco.NET to do what's called Continuous Integration (CI). You can read about it here, but the basic idea is to build every time code is checked in. I've found it to be enormously useful for letting me know immediately when I've checked in non-working code, or code that breaks something someone else is working on.
Here's the problem: Draco.NET runs as a service. In a non-interactive window station. Which means that Direct3D refuses to initialize, just like it would if you somehow tried to fire up a game while a screensaver was running. So my unit tests were all failing, and as a result, the project refused to build.
I had two choices. My preference was to figure out a way to get CI working with Direct3D. But after goofing around with it for a while, I realized that either I was going to have to set up the CI service to run as SYSTEM (to allow interation with the desktop) or I'd need to convert the CI system to run in a logged in session. Direct3D just needs to run in an interactive window station.
Since I use the build machine for other purposes, neither of those was attractive. I was going to wind up dealing with having to stay logged in all the time (and disabling the screensaver, too), or having random windows pop up and obscure my work from time to time. No thanks.
So here I am, back at the Mock Object stage. It looks like I'll need to create an IGraphicsDevice interface that will wrap up either a RealDevice or a MockDevice object. RealDevice will call all the Direct3D objects. MockDevice will just simulate those calls, allowing me to test my game logic. It's going to involve a performance hit, but I'm not sure how much of one - my game is 2D, fairly simple, and written in C#, so it probably doesn't matter.
Oh well - at least I can reuse the objects elsewhere when I'm done.
Minor point: the screensaver shouldn't be a problem. All that happens when the screen saver fires up is that the foreground desktop switches to the screensaver desktop (and I think even this only happens with a password protected screensaver, but I'm sure you've got that). The interactive winstation stays around, and your tests should continue to run just fine.
ReplyDeleteIt's interesting to watch you going down the TDD/CI path, while at the same time I've got Brad Wilson and company at the Denver Pragmatic Practitioners inviting me to come to their shindigs in Denver. Brad was just telling me about CI, and I told him that I figured that was what Craig was up to given the questions he was asking me about running graphic tests in the background ;-)
So, I can't remember exactly what happened in my experiements now, but it may be that I'm thinking of fast user switching rather than screensavers. More than one person uses that computer, so we need to be able to have someone logged in while the test is running.
ReplyDelete