Kotlin 196 - Catching up some tests
GitHub Decentralized Repo
GitHub Centralized Repo
Today, I was hoping to finish up the new centralized wave-making. But first I need to bring my testing up a bit. Added in Post: Includes Ron’s Jira.
- Added In Post
- I thought I’d work on finishing up centralizing wave making. Turns out I’m mistaken.
I think we are down to steps 4 and 5 from yesterday’s plan:
- Capture DeferredAction instances in a separate collection in the
knownObjects
SpaceObjectCollection, as well as in the general mix; - Change
knownObjects
not to add them to the mix, and in the same commit, cause Game to update them directly; - Provide a way to count existing asteroids, probably in
knownObjects
; - In Game, implement a makeWave function;
- Cause Game to check asteroid count and create the DeferredAction to make the wave, and stop adding WaveMaker to the mix.
I had that working yesterday, and most of the details are in yesterday’s article, should I need to refer to them. But I also left some notes and I even had a thought. Before I do new code, let’s sort out the notes.
Thought
The “thought” was that in testing moving WaveMaker out of the mix and into the centralized part of the program, I felt the need to play the game in order to see that it worked. I did find some serious issues doing that, but when we feel the need to run the program to test it, it’s at least a hint that some executable tests might be needed. I’ll add item 4 below to reflect that.
Notes
There are three red notes on my keyboard tray, which I call “Jira”.
- Added In Post
- Picture of Jira with two of today’s red notes removed, because finished:
- Need a test showing the need for
removeAndFinalizeAll
to useaddAll
, not just +=; (not shown in pic) - Need a test showing the need for
SpaceObjectCollection.clear
to clear all the sub-collections; Note: Should try to find a way to ensure this connection; (not shown in pic) - Remember to cancel
oneShot
when setting up new game; - Probably need a test to ensure that asteroids are created.
Let’s start with #4, because it will be useful this morning. Maybe that will give me enough momentum to do the other tests, which shouldn’t find any problems because I already fixed the issues.
- Added In Post
- It turns out there was enough testing to do to take me up to needing a break. Nothing below here but testing and thinking. Some of it’s good thinking. Skim, don’t read? I don’t care: you do you.
Ensure asteroid creation
This seems like a test of Game, so I’ll put it in GameTest class. After some difficulty …
@Test
fun `game creates asteroids after a while`() {
val game = Game()
val controls = Controls()
game.createInitialContents(controls)
assertThat(game.knownObjects.asteroidCount()).isEqualTo(0)
game.cycle(0.2)
game.cycle(0.3)
game.cycle(4.2)
assertThat(game.knownObjects.asteroidCount()).isEqualTo(4)
}
I had two difficulties with this test. First, I was putting in deltaTime values in the calls to cycle, and it expects elapsed. The fact that IDEA displayed elapsedTime
on every line did not help me. The second difficulty is that in the decentralized mode, things happen sort of “eventually”. You need a few ticks for one object to decide to create another, then a few more for that one, and so on. I winnowed the calls to cycle
down to three after I got it working with about seven. Anyway, now it runs.
I think we need another test like this one, because yesterday I saw that the game would only create asteroids when you inserted a quarter if it had created them on the attract screen. The issue was the one referred to in the list above, that Game was not cancelling the OneShot that it was using for wave-making, and therefore, if it hadn’t had time to trigger, it wouldn’t fire again.
So we need a test that says something like this:
@Test
fun `game creates asteroids even when quarter comes rapidly`() {
val game = Game()
val controls = Controls()
game.createInitialContents(controls)
assertThat(game.knownObjects.asteroidCount()).isEqualTo(0)
game.cycle(0.2)
game.cycle(0.3)
game.insertQuarter(controls)
game.cycle(0.2)
game.cycle(0.3)
assertThat(game.knownObjects.asteroidCount()).isEqualTo(0)
game.cycle(4.2)
assertThat(game.knownObjects.asteroidCount()).isEqualTo(4)
}
That runs green. Commit: additional tests of wave creation.
This is boring, but let’s do the other tests as well.
- Lesson
- There is a lesson here that I need to learn and relearn, over and over, ad infinitum, a lot: It’s more fun to write tests for things that don’t work yet, because I get a little jolt of reassurance when they run after having first shown that the feature isn’t there. It’s a lot less fun to write tests like these, that merely confirm that something is already done. Of course, tests like these do sometimes turn up problems, but because I expect them to work, I’m not that thrilled when they do.
-
Testing before it works is more fun than testing afterward.
Ah well, press on. I decide on this test:
@Test
fun `clear clears all sub-collections`() {
val s = SpaceObjectCollection()
s.add(Missile(Ship(U.CENTER_OF_UNIVERSE)))
s.add(Asteroid(Point.ZERO))
s.add(WaveMaker())
val deferredAction = DeferredAction(3.0, Transaction()) {}
s.add(deferredAction)
s.clear()
for ( coll in s.allCollections()) {
assertThat(coll).isEmpty()
}
}
This asks for a new function, allCollections
, which I intend to use to clear as well as to test. Watch.
class SpaceObjectCollection {
var scoreKeeper = ScoreKeeper()
val spaceObjects = mutableListOf<ISpaceObject>()
val attackers = mutableListOf<ISpaceObject>()
val targets = mutableListOf<ISpaceObject>()
val deferredActions = mutableListOf<ISpaceObject>()
// update function below if you add to these
fun allCollections(): List<MutableList<ISpaceObject>> {
return listOf (spaceObjects, attackers, targets, deferredActions)
}
I expect the test to run, unfortunately. Undaunted, I change clear()
to make it fail:
fun clear() {
spaceObjects.clear()
}
I get two failures for the price of one, because the test for creating asteroids we just wrote fails with 8 rather than 4.
I note that the test I just wrote provides no info as to which collection wasn’t cleared. That’s troubling but I don’t see a quick way to improve it. I’ll fix clear()
:
fun clear() {
for ( coll in allCollections()) {
coll.clear()
}
}
Green. Nice. That was a bit more fun than a test with no added value. Commit: Test ensuring all sub-collections are cleared on clear().
Whew. Now the removeAndFinalize
one. What even was that?
Oh look, this is well worth doing:
fun removeAndFinalizeAll(moribund: Set<ISpaceObject>) {
moribund.forEach { addAll(it.subscriptions.finalize()) }
removeAll(moribund)
}
private fun removeAll(moribund: Set<ISpaceObject>) {
spaceObjects.removeAll(moribund)
attackers.removeAll(moribund)
targets.removeAll(moribund)
deferredActions.removeAll(moribund)
}
It used to be, if I recall, that we just removed things from spaceObjects
. This removeAll function can use our new allCollections
function. Let’s do a test. I think this time I’ll just jam things into the various collections, and I’ll make removeAll
public and test it.
@Test
fun `removeAll removes from all collections`() {
val s = SpaceObjectCollection()
val toRemove: MutableSet<ISpaceObject> = mutableSetOf()
for ( coll in s.allCollections()) {
val toAdd = Asteroid(U.CENTER_OF_UNIVERSE)
toRemove.add(toAdd)
coll.add(toAdd)
}
// s.removeAll(toRemove)
for ( coll in s.allCollections()) {
assertThat(coll).isEmpty()
}
}
I’ve commented out the remove, so as to get the failure. The test is a bit odd but it will always ensure that all collections are tried.
Test fails as intended. Uncomment the remove. Green. Commit: Test ensuring removeAll
removes from all collections.
I think we should do the same test on the finalize
method, though. After all, it was the one that failed before. For that to work, I need to insert some object with that doesn’t create anything in finalize. A Score should work.
@Test
fun `removeAndFinalizeAll removes from all collections`() {
val s = SpaceObjectCollection()
val toRemove: MutableSet<ISpaceObject> = mutableSetOf()
for ( coll in s.allCollections()) {
val toAdd = Score(666)
toRemove.add(toAdd)
coll.add(toAdd)
}
s.removeAndFinalizeAll(toRemove)
for ( coll in s.allCollections()) {
assertThat(coll).isEmpty()
}
}
That runs. Commit: Test ensuring removeAndFinalizeAll
removes from all collections.
I know there’s another issue that might need testing, but I can’t find it. There was a place in there somewhere that just added objects to knownObject
rather than calling add
or addAll
. I can’t find it now, but I think these tests will cover it.
I’m about two hours in, and I think it’s time for a break. Let’s quickly sum up, maybe do more later.
Summary
This session was all testing. It was more fun than I had thought it would be, because I got to implement the new allCollections
idea, which should serve to keep us safe from additional collections added to knownObjects
, should that need to happen. (I don’t think it will, but what do I know?)
A couple of the tests we did are directed at wave making, and cover actual problems that arose yesterday in the spike. So they should make me more comfortable with running the tests more and the game less. You can be sure, however, that I will test in the game. This is perhaps the best-tested video game I’ve ever coded, but I still feel the need to see it running.
Would it be possible to have such solid tests that we wouldn’t play the game, we’d just ship it? It’s hard for me to imagine that level of confidence with something so visual … but it would be great if it could be done. These tests will get me closer.
The big lesson, which I’ve learned many times, is that testing before coding is more fun and provides more energy than testing after. TDD is such a pleasant way to work for me. Write a little test, see it fail, make it work, feel good, repeat.
Next time I’m pretty sure we’ll get to doing the centralized wave-making. I think we’re ready. I hope you’ll join me then!