Logan Barnett
|
Posted: January 11, 2010 21:31 by Logan Barnett
|
|
One thing that's been on our mind for a while is how to do test first with Jemini. I want to stay away from terms like BDD, TDD, and integration testing because a game/simulator is a totally different realm than a sequential webapp or GUI app, and we shouldn't limit our thinking about how we approach things. It's highly recommended that any complex game logic be moved out to a behavior. That behavior is an API that your game designer (even if that's you) will consume. That behavior should be tested with your favorite unit testing framework (like RSpec). Have have lots of examples for various behaviors in the Jemini code itself if you need an example. So how do we do our high level integration tests? This is what I came up with using Cucumber, my integration testing tool of choice, on the Life Tank. LifeTank is our most polished game at time of writing.
I could refactor a few of these, but it's mostly going to be custom stuff. A lot of physics gets used in LifeTank, but it's hard or not useful to pull those out into steps. Another facet is that LifeTank has randomly generated terrain. Some slopes in the terrain are deliberately too steep to climb. This makes it hard to test moving without using some completely different configuration - at which point we aren't doing an integration test. I think the best way to tackle this is view the game as a system or a simulation. We want to see a lot of objects interacting with each other in similar ways, and more importantly we want to the game to be testable in a way that allows us to define what's fun. Fun means the tank shells go up and come down just right. It means that the shells can only be fired so far, etc. It's hard to get a machine to do this in precise, numerical terms. I'd like to see an app that allows us to run a mocked version of our game in an automated fashion, but still allow us to verify that things worked well. This can be used in a test-first manner that allows us to tweak code and values until we get things right where we ant them. These are my stories I've written up for this app:
* is some nice features that won't get done on version 1 Let me know what you think. I think this could be a really helpful tool for us going forward! |
Testing in Jemini
by: jaymcgavren
jaymcgavren
|
Posted: January 12, 2010 01:12 by jaymcgavren
|
|
This "live tests for fun" environment sounds cool (maybe even necessary). But I find it off-putting that we have no Cucumber tests for more precise scenarios, and I think other Ruby devs will too. You're concerned about randomness - it is the job of the test author to eliminate that randomness. Here's a re-write of your first example to eliminate the ambiguity... Scenario: Moving Given there is a test level And the level uses physics And there is flat ground (generate this in Cucumber steps - isolate the randomness) And gravity is set to 5.0 And there is a tank tied to player 1 And the tank is positioned at 200, 400 When player 1 holds the "a" key for 5 seconds Then the tank's x position is about 0 (Or substitute "less than 100", "near 90", etc.) All I had to do was flatten the ground (setting the y position of all its points to equal values shouldn't be too difficult). Using specific number makes writing the steps easier. Second scenario: Scenario: Shooting Shells Given there is a test level And the level uses physics And there is ground And gravity is set to 5.0 And there is a tank tied to player 1 And there is a tank tied to player 2 And player 1's tank is positioned at 200, 400 And player 2's tank is positioned at 250, 400 And player 1's tank's angle is at 90 degrees When player 1 presses the "space" key And the game runs for 0.5 seconds Then there should be a shell game object When the game runs for 2 seconds Then player two is not at full health And the shell is gone Again, swap in a few specific numbers, and the steps get a lot more generic and easier to write. Hell, "position" can be used for almost every Spatial ever made. I'm all in favor of experimenting with the "live test" environment. But it must not be done at the exclusion of Cucumber tests. We don't even have to include step files in the core library, but we darn well better make sure it's possible to write them. |
Logan Barnett
|
Posted: January 12, 2010 19:04 by Logan Barnett
|
|
Jay, My main concern with Cucumber tests is that Cucumber seems to be really good for integration testing, but what you're seeing there is something along the lines of RSpec testing - instead of starting the game and playing it, we're now just seeing how a few particular things operate in highly controlled environments. This is not a bad thing, but I don't think we can truly achieve integration testing. That said, your steps are much better than mine. I'm open to us going both directions at the same time, but I'd like your help with the Cucumber steps (and what level of steps we're going to define for all of our behaviors). Just at a gut I think simulation tests vs cucumber is going to take a relatively equal amount of work. Which is more useful to us now and going forward? |
jaymcgavren
|
Posted: January 13, 2010 02:55 by jaymcgavren
|
|
> Just at a gut I think simulation tests vs cucumber is going > to take a relatively equal amount of work. Which is more > useful to us now and going forward? I'm going to say Cucumber. The simulation thing hasn't been done, so the burden of proving otherwise is on you. ![]() Already started on some sample steps to include with the generator, though I got thwarted by Given /(.+) is at position (x), (y)/ and such, due to the need to convert $1 to an object reference. (Hard to do in a sufficiently flexible fashion.) Should be easy to set up game states, physics managers and so forth, though. |
Logan Barnett
|
Posted: January 15, 2010 04:44 by Logan Barnett
|
|
Remember that Jemini stole Rails' inflector. You can camelize.constantize on the string you get back. You aren't actually using the $n vars are you? (: |
jaymcgavren
|
Posted: January 15, 2010 06:04 by jaymcgavren
|
|
No, $1 was a bit of shorthand on my part. Getting the class name from the matched string isn't the problem, it's passing it to a helper module (similar to the paths.rb that cucumber-rails generates), and always getting the same object back. Here's what I want to do, similar to the paths.rb that cucumber-rails generates:
module ObjectHelpers
def object_for(name)
case name
when /the tank/
@tank
# Add more mappings here.
else
raise "Can't find mapping from \"#{name}\" to an object.\n" +
"Now, go and add a mapping in #{__FILE__}"
end
end
end
World(ObjectHelpers)
That way I can have common steps like: Given /^(.+) is located at ([\-\d\.]+), ([\-\d\.]+)$/ do |name, x, y| object_for(name).location = Vector.new(x.to_f, y.to_f) end But paths.rb gets to just return a String with the resolved path; I have to return an object. That @tank is local to ObjectHelpers, not the spec. Not sure how to fix this - passing bindings around or doing a bunch of instance_evals would get messy fast. This is only a problem with common steps; obviously "Given /^the tank is located at ([\-\d\.]+), ([\-\d\.]+)$/" (as opposed to "(.+) is located at...") can just reference @tank created elsewhere within the same spec. I've moved on; better to get Cucumber working in the first place and worry about what common steps we'll generate later. |
Logan Barnett
|
Posted: January 15, 2010 19:46 by Logan Barnett
|
|
Just make the method global (on Object). I have some tests for a project that do the same thing. You're pretty much making it global anyways by including it in the world. Our behavior system should prevent collisions with object_for if someone gets clever and tries to use that for a production behavior d: |
jaymcgavren
|
Posted: January 15, 2010 23:20 by jaymcgavren
|
|
Well, if I modified Object (and I am very reluctant to do that in *any* context) it would only be via features/support/env.rb. So the method wouldn't exist outside of Cucumber specs. Blargh, does that mean I have to keep the objects in a global hash? $cucumber_objects['the tank'] = Tank.new? I might prefer leaving objects out of the common steps over writing a kludge like that. |
Logan Barnett
|
Posted: January 18, 2010 16:14 by Logan Barnett
|
|
No, you can still use instance variables. I took your existing object_for code and modified it. If you make a file and drop this in the support dir (env gets generated typically, so leave it alone), you'll get the desired effect without feeling like a giant kludge:
Note the lack of module and World call. This puts a method on Object since the context of execution is on Object. Granted, everything gets object_for. However, it's contextual with use of instance vars. Since it's on object, it's also the first thing to be overridden if someone decides to get cute. |
jaymcgavren
|
Posted: January 20, 2010 07:26 by jaymcgavren
|
|
Well, I started out by trying to load an entire state, but I'm getting thwarted bigtime. Any attempt to load images or sounds into Slick fails unless you're in init() or the game loop (and of course, every real-world state is going to do that). That means creating a Game and AppGameContainer, but the init methods for those have so many side effects that I don't see a way to just load the game; seems like you *have* to actually start it. I can of course start stubbing stuff (already tried to in the ResourceManager, but that introduces more problems), but that's gonna take quite a bit of experimentation. My next attempt is going to involve loading the desired Managers and GameObjects via explicit steps. After that, well, I'm giving up and waiting to see what Logan comes up with for a live test environment. Revisiting Cucumber might be more productive after the transition to JME (and I will definitely be pushing for the ability to set up a game environment without actually running it). I *am* confident that I can set up RSpec within a newly-generated project. That should ease unit testing, at least. |
gustin
|
Posted: January 12, 2010 16:56 by gustin
|
|
Awesome, this was exactly what I had in mind to help introduce BDD with Jemini as a teaching tool. I tend to be as high-level as possible with cucumber tests, not sure about the data having to be so specific (tank at 200, 400). But data specifics could be wrapped in reusable steps. I'd love to chip in on this integration if help is needed. |
jaymcgavren
|
Posted: January 12, 2010 18:58 by jaymcgavren
|
|
For the Cucumber portion, I'd want to generate a features directory, Cucumber tasks for Rake, and some base sample steps (a la Webrat's Cucumber integration) when a new project is generated. But since there's a physics and/or graphics engine involved, we're at least partially blazing a trail here. (Not sure how exactly to handle the "game runs for 5 seconds" step, for example - do we actually have to run for 5 seconds to avoid timing issues?) So I think experiments would be necessary. Gustin, if you wanted to go the Cucumber route, I'd say the best thing would be to set it up in your own project. If you get some reusable sample steps, we could then add them to generated Jemini projects. That way you won't have to wait on us to roll something out, either. For the live test environment, well, I suggest sitting back and seeing what Logan comes up with. If it's anything like his map editor, it won't take him long, and it'll be a unique and elegant solution. I'll set to work on a features directory generator that can be used across projects tonight. |
Logan Barnett
|
Posted: January 12, 2010 19:08 by Logan Barnett
|
|
You can rapidly apply deltas to the app's update method to simulate the passage of time. I recommend this is the only way that time elapses in line-based tests, because you don't want assertions on a different thread elapsing time, or allowing an update to go by between assertions. |
by: jaymcgavren









